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 ASEC survey vintages 2022/2023/2024, and the 2022 and 2023 per-year raw H5 inputs never carried the NOW_* at-interview coverage recodes (except 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 survey vintages. source_year on the certified person table: 2022 (54,464 rows, 106.8M weighted), 2023 (54,654 rows, 105.7M), 2024 (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 lack the NOW_* coverage block. 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 matches the archived raw-lane converter: policyengine-us-data/datasets/cps/census_cps.py carries NOW_GRP and NOW_MRK in its person column list, and NOW_MCAID never appears anywhere in that file's git history (git log -S NOW_MCAID is empty); the 2024-vintage input was produced by a newer extraction that includes the full block. 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 are not the drivers. 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 frame's distinct retained ASEC-2023 source persons back to pppub23.csv by PERIDNUM: they capture 9.50% of full-survey under-65 population mass, 9.41% of NOW_MCAID mass, and 9.62% of NOW_GRP mass (original MARSUPWT) — household selection is coverage-neutral. If all three vintages carried the recodes at survey rates, the artifact would report ≈ 0.211 × 278.9M ≈ 59M under-65, consistent with the survey.

  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 2022 and 2023 per-year raw ASEC H5s with the full NOW_* at-interview coverage block and rebuild the pooled source base. The raw CSVs carry the data — verified for ASEC 2023 (pppub23.csv has NOW_MCAID, column 547); confirm ASEC 2022 at re-extraction. This runs in the gated data lane outside PR CI; no artifact publication is part of this issue.
  2. Regression guard (small, PR-able now): 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