Skip to content

Pooled ASEC 2022/2023 vintages lack the NOW_* coverage recodes: reported Medicaid at interview thins to 24.6M vs ~58M survey under 65 on the certified artifact #720

Description

@MaxGhenis

Summary

On the certified default artifact (hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z), the reported-coverage person input has_medicaid_health_coverage_at_interview sums to 24.6M weighted under-65 persons versus ~57.5M survey-reported (CPS ASEC 2023 NOW_MCAID, MARSUPWT). Root cause: the pooled support spine stacks three ASEC vintages keyed by income year (source_year 2022/2023/2024 = the ASEC 2023/2024/2025 survey files), and the income-year-2022 and -2023 raw H5 inputs carry only 2 of the 18 NOW_* at-interview coverage recodes (NOW_GRP and NOW_MRK). The CPS-carried derivation maps missing/NaN source values to False, so roughly two-thirds of the weighted population — every 2022- and 2023-vintage person, 173.2M weighted under 65 — is structurally unable to report Medicaid (or TRICARE, VA, CHAMPVA, IHS, other-means-tested, or non-marketplace direct-purchase) coverage.

has_esi and has_marketplace_health_coverage_at_interview escape only because NOW_GRP and NOW_MRK happen to be the two NOW_* columns the archived raw-lane converter carried for all vintages.

Downstream consequence (policyengine-scorecard PR #69, annotation kff-medicaid-reported-denominator-sanity): the "no reported coverage" construct identifies 80.9M under-65 persons versus 25.9M survey uninsured-at-interview (NOW_COV==2, ASEC 2023) and ~27M ACS nonelderly uninsured, so uninsured-population constructs on the certified artifact are not survey-faithful. Modeled enrollment is separately CMS-calibrated and does not backfill the reported flag.

No artifacts were published or promoted as part of this investigation.

Reproduction

Weighted person counts on the certified artifact (person-table columns × household weight; the columns are physically present in the h5 and the engine surface reads them as-is), against full-file CPS ASEC 2023 (MARSUPWT/100):

input (under 65, millions) artifact ASEC 2023 survey survey column
has_esi 146.9 164.7 NOW_GRP
has_marketplace_health_coverage_at_interview 21.4 11.4 NOW_MRK
has_non_marketplace_direct_purchase_…_at_interview 4.0 8.8 NOW_NONM
has_medicaid_health_coverage_at_interview 24.6 57.5 NOW_MCAID
has_champva_health_coverage_at_interview 0.3 0.4 NOW_CHAMPVA
has_tricare_health_coverage_at_interview 3.5 6.0 NOW_MIL
has_va_health_coverage_at_interview 0.7 1.6 NOW_VACARE
has_other_means_tested_…_at_interview 0.5 0.7 NOW_OTHMT
has_indian_health_service_coverage_at_interview 0.3 0.8 NOW_IHSFLG
none of the nine ("no reported coverage") 80.9 25.9 NOW_COV==2

The marketplace flag tracks NOW_MRK one-to-one on all vintages, so its level above the ASEC 2023 survey reflects vintage composition and calibration-era weighting, not the missing-column mechanism (out of scope here).

Artifact totals: 340.1M persons, 278.9M under 65 (57,240 households, 166,321 person rows; pe-us pin 1.764.6 per the build manifest).

Compact repro (certified snapshot cached locally)
import os, h5py, numpy as np
p = "~/.cache/huggingface/hub/datasets--policyengine--populace-us/snapshots/26dcad66867687f15735dc4926523e3741920836/populace_us_2024.h5"
f = h5py.File(os.path.expanduser(p), "r")
per, hh = f["person/table"][:], f["household/table"][:]
hw = dict(zip(hh["household_id"], hh["household_weight"]))
w = np.array([hw[h] for h in per["person_household_id"]])
u65 = per["A_AGE"] < 65
flag = per["has_medicaid_health_coverage_at_interview"].astype(bool)
print(w[flag & u65].sum() / 1e6)          # 24.6
for yr in (2022, 2023, 2024):             # vintage split
    m = per["source_year"] == yr
    print(yr, w[m & flag & u65].sum() / 1e6, np.isnan(per["NOW_MCAID"][m]).mean())
# 2022  0.0  1.00 / 2023  0.0  1.00 / 2024  24.6  0.00

Root cause

  1. The pooled spine stacks three ASEC vintages keyed by income year. source_year on the certified person table: 2022 = ASEC 2023 survey (54,464 rows, 106.8M weighted), 2023 = ASEC 2024 (54,654 rows, 105.7M), 2024 = ASEC 2025 (57,203 rows, 127.6M). Pooling unions per-year columns via pd.concat (asec_pool.py#L141), leaving NaN where a vintage lacks a column.

  2. The 2022/2023 raw H5 inputs carry only 2 of the 18 NOW_* fields. On the certified frame, NOW_MCAID, NOW_CAID, NOW_COV, NOW_MRKS, NOW_MRKUN, NOW_PCHIP, NOW_CHAMPVA, NOW_VACARE, NOW_MIL, NOW_IHSFLG, NOW_NONM, NOW_OTHMT, NOW_PUB, NOW_PRIV, NOW_DIR are populated on exactly the 57,203 2024-vintage rows and NaN on all 109,118 rows from 2022/2023. Only NOW_GRP and NOW_MRK are populated for all vintages. This is a property of the SHA-locked inputs themselves: policyengine_us_data/storage/census_cps_2022.h5 (146,133 persons = pppub23, 100% PERIDNUM match; 137 person columns; NOW_* = {NOW_GRP, NOW_MRK}), census_cps_2023.h5 (144,265 persons = ASEC 2024; 138 columns; the same two), census_cps_2024.h5 (142,125 persons = ASEC 2025; 162 columns; all 18 NOW_* fields). The 2022/2023 inputs match the archived policyengine-us-data extractor's column list before 410f63a7 ("Add CPS current health coverage anchors", 2026-03-20, which added the broader block); the 2024 input was extracted with it. The input-coverage manifest already documents this vintage asymmetry for other fields ("The 2022 and 2023 inputs also omit PECOHAB, A_EXPRRP, and A_FAMREL; only 2024 carries those alternatives" — is_unmarried_partner_of_household_head exclusion; same pattern in the employer_sponsored_insurance_premiums exclusion, Track SPM-specific input gaps: housing, WIC, school meals, child support, workers comp, and expense deductions #32).

  3. The CPS-carried derivation silently maps missing to False. _fill_health_coverage_inputs maps has_medicaid_health_coverage_at_interview ← NOW_MCAID (L272) through _yes_code, where _source returns zeros for an absent column and fillna(0.0) for NaN. The certified flag equals NOW_MCAID == 1 exactly (weighted crosstab: 24.6M both-true, 0.0M in either disagreement cell) — no later stage rewrites it, and the export carries it (this is not the Sparse-57k export drops 94 no-formula input columns from the dense parent (tips, overtime, disability flags, ESI premiums, rent, QBI basis) #361 export-drop class; the column is present).

  4. The 2024-vintage slice is survey-plausible; selection and reweighting push the other way. Within the 2024 vintage the flag carries 24.6M of 105.7M under-65 = 23.3%, versus 21.1% in full ASEC 2023 (57.5M of 272.7M). Joining the 2022-vintage rows back to pppub23.csv by PERIDNUM (100% match, zero age mismatches), the under-65 Medicaid-at-interview rate moves from 21.1% in the full survey to 23.8% among the selected source persons, 24.5% with support clones, and 25.8% under final artifact weights — selection and calibration mildly enrich reporters, the opposite direction from the thinning. Counterfactual scale: restoring the raw codes on the artifact's existing rows and weights gives ≈ 70M under 65 (22.7M on the 2022 vintage, verified directly against pppub23; the 2023 slice extrapolated at the same enrichment; 24.6M already on 2024); at survey rates ≈ 0.211 × 278.9M ≈ 59M. A rebuild reselects and reweights, so neither is a guaranteed post-fix value — the point is the scale, ~2.5–3× today's 24.6M.

  5. The Medicaid take-up stage consumes the already-thinned anchor. medicaid_take_up.py anchors on the flag (eligible reporters always take up) and hard-fails only if the column is absent (L173-L177) — there is no guard on per-vintage coverage. The CMS state-count fill compensates in enrollment terms, so enrollment-side calibration passes while the reported-coverage denominator stays thinned; the thinned anchor also shifts enrollment composition from reported toward filled and overstates saturation pressure (Add Medicaid and CHIP eligibility-to-enrollment diagnostics #170's diagnostic direction).

Why certification passed

  • release_input_coverage requires the column to be present with non-default signal ("Column mass parity is not column coverage" — the gate closes the absence class, Build J: full eCPS-exported-column coverage as a HARD release gate + asset-column restoration (SSI reforms score $0) #368); 24.6M true satisfies it.
  • degenerate_input_signal (175 columns) passes any column that is neither all-default nor constant.
  • health_input_signal checks 2 columns (selected_marketplace_plan_benchmark_ratio, takes_up_aca_if_eligible) — no reported-coverage flags.
  • ecps_parity is a populated-layer presence check (158 layers, 0 gaps).

Nothing measures per-vintage signal or survey-mass fidelity for the reported-coverage family, so a flag populated for one of three vintages certifies.

Relationship to existing issues

Proposed fix

  1. Data side (the actual fix): re-extract the income-year 2022 and 2023 inputs (ASEC 2023/2024) with the full Census person column set — not a NOW_*-only patch, so the next missing field (Track SPM-specific input gaps: housing, WIC, school meals, child support, workers comp, and expense deductions #32's NOW_OWNGRP/NOW_HIPAID/NOW_GRPFTYP, Populate remaining US tax-input and reported-observation layers #38's PERRP/A_EXPRRP/PECOHAB) does not repeat this cycle, while any new derivations stay in separate PRs — and rebuild the pooled source base. Do it as one schema migration with Ingest ASEC 2024 and 2025 into the US support pool (spine is two vintages behind) #296 (whose body predates the ASEC 2025 ingest: Build P already consumes income years 2022–2024) after reconciling survey-year vs income-year naming. The raw CSVs carry the data — verified for ASEC 2023 (pppub23.csv has NOW_MCAID, column 547). This runs in the gated data lane outside PR CI; no artifact publication is part of this issue.
  2. Regression guard (PR Add per-vintage reported-coverage signal gate (#720) #744): a per-vintage reported-coverage signal gate — for each pooled source_year, every reported-coverage input must carry non-default signal (the crisp invariant violated today: 2022/2023 vintages have exactly zero true mass for seven of the nine flags). Ships red against current bases by design, like the Build J: full eCPS-exported-column coverage as a HARD release gate + asset-column restoration (SSI reforms score $0) #368 asset gates, and turns green with the re-extraction.
  3. Scorecard follow-up: once a fixed artifact certifies, the kff-medicaid-reported-denominator-sanity annotation and the reported-uninsured rows in policyengine-scorecard PR Target latest eligible Ledger fiscal facts #69 can be revisited.

Numbers computed 2026-08-18 on the locally cached certified snapshot (26dcad668676…, sha256 48b9d479fb4f…) and asecpub23csv with policyengine-us 1.764.6 (the build pin).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions