Skip to content

Carry CPS educational attainment as an auxiliary column and bind S1501 state cells (#297) - #1356

Draft
DTrim99 wants to merge 2 commits into
mainfrom
cps-educational-attainment
Draft

DTrim99 wants to merge 2 commits into
mainfrom
cps-educational-attainment

Conversation

@DTrim99

@DTrim99 DTrim99 commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #297 (the student debt pilot, Project 2: PolicyEngine/us-student-debt#1, ROI task us-student-debt#5).

Summary

The US fiscal-refresh build can now carry educational attainment as an auxiliary person column, cps_educational_attainment, beside the raw Census A_HGA it is classified from. The US target compiler binds the ACS 1-year S1501 state cells for the population 25 and over as person counts of that column, and the tool's materializer counts them. Nothing binds in a release yet: the pinned Chronicle feed has no S1501 facts, and they arrive with PolicyEngine/chronicle#347.

  • --cps-educational-attainment (opt-in) joins each person to the pinned official CPS ASEC person member on (income year, PERIDNUM) and checks the Census household (source_household_id = PH_SEQ), P_SEQ, A_LINENO and A_AGE. It never predicts or defaults a value, and a PUF support clone takes its source person's code. --asec-attainment-source INCOME_YEAR=PATH supplies local archives; unmapped years are fetched and verified against the same pins (spm_role_source.ASEC_SPM_ROLE_SOURCES). A signal gate refuses missing or unknown values, a universe mismatch, and implausible below-diploma or bachelor's-or-higher shares.
  • The stage refuses ACS-spine rows (no PERIDNUM).

Classes and code mapping

Seven classes, the S1501 population-25-and-over categories (Chronicle value ids reused), plus not_in_universe. Merging the first two gives the six classes the issue asks for (US_EDUCATIONAL_ATTAINMENT_SIX_CLASSES).

Class CPS ASEC A_HGA ACS PUMS SCHL
less_than_9th_grade 31-34 01-11
9th_to_12th_grade_no_diploma 35-38 12-15
high_school_graduate_includes_equivalency 39 16, 17
some_college_no_degree 40 18, 19
associates_degree (occupational 41 + academic 42) 41, 42 20
bachelors_degree 43 21
graduate_or_professional_degree 44, 45, 46 22, 23, 24
not_in_universe 0 ("Children", under 15) blank (under 3), or any person under 15

Sources checked code for code:

  • A_HGA, "Item 18h - Educational attainment": the ASEC public-use data dictionaries for survey years 2023-2026, https://www2.census.gov/programs-surveys/cps/datasets/<YYYY>/march/asec<YYYY>_ddl_pub_full.pdf (PDF pages 22, 23, 24, 28). All four list 0 and 31-46 identically. In the three pinned members, A_HGA == 0 exactly when A_AGE < 15.
  • SCHL: https://www2.census.gov/programs-surveys/acs/tech_docs/pums/data_dict/PUMS_Data_Dictionary_2024.txt.
  • S1501: rows S1501_C01_006E-013E (25 and over) and 001E-005E (18 to 24).

The SCHL map is defined and tested, but the ACS-local lane does not read SCHL from PUMS yet (out of scope here).

ROI classes (US_DEGREE_ROI_CLASSES): two-year degree = associates_degree, four-year degree = bachelors_degree, each against the high_school_graduate_includes_equivalency baseline.

Column choice

A Microcosm auxiliary column, not a policyengine-us input: the analysis joins it on person_id, and no policyengine-us change is needed. Recorded on #297: #297 (comment).

For the published default populace-us-2024-spm-20260915, the receipt run also wrote a person_id, A_HGA, cps_educational_attainment sidecar (166,321 rows, sha256 34994413e255524f16ab9d985bd8ca8470deab1149b3ee395d2ee307c9d7e62b). It is in the run's Modal volume (microcosm-297-cache, results/20261022T171517Z-e2621e231/) and is not committed, so the ROI work can start before a new build exists.

Provenance flag

microcosm.data.stored_inputs.US_AUXILIARY_STORED_COLUMNS is a new register of auxiliary columns, carried for conditioning and calibration and not consumed by rules. It is merged into US_STORED_NON_VARIABLE_COLUMNS, so the stored-input gates accept the column. Its reason starts with "Auxiliary:" and names its producer, and the register tests bind it to educational_attainment.US_CPS_EDUCATIONAL_ATTAINMENT_COLUMN. The stage's receipt (us_cps_educational_attainment.json in the build artifacts) records column_role: auxiliary.

Targets and compile path

  • fiscal_targets._acs_attainment_reference_from_fact admits census_acs.acs1_<year>.s1501.state_attainment_25_plus.<st> detail rows for the 50 states and DC. The family is census_educational_attainment, the materializer population_attainment, and the person count is taken over the cell's half-open age band. It leaves the universe totals unbound, because PEP binds population by state and age. It also leaves the 18-to-24 universe unbound (the ACS counts dormitory residents where they study), and Puerto Rico. A changed category, age band or measure raises instead of binding.
  • The tool's _population_attainment_household_values counts the cell, and population_attainment joins the materializers that accept a restated age bound. A frame without the column refuses to materialize these targets rather than binding zeros.
  • fiscal_target_references.json's CD-to-state reconciliation rule matches on educational_attainment_category too, since one state's seven rows differ only in class.
  • New category label census_acs.educational_attainment.

State totals vs S1501 (receipt)

experiments/297-cps-educational-attainment-receipt.md (JSON: experiments/receipts/297-cps-educational-attainment-s1501.json). The stage ran on the published default at its calibrated weights; the S1501 facts were compiled through the real compiler (357 targets, 0 mismatches against the raw Census response) and materialized with the tool's own code.

National, 25 and over:

Class S1501 Release
< 9th grade 4.6% 3.3%
9th-12th, no diploma 5.5% 5.0%
High school or GED 25.7% 27.9%
Some college, no degree 18.5% 14.1%
Associate's 8.8% 11.1%
Bachelor's 22.1% 24.2%
Graduate or professional 14.7% 14.3%
  • Across the 357 state cells, the mean absolute share error is 2.29 pp (90th percentile 5.21 pp, max 10.84 pp, MS high school). For the ROI classes it is 2.45 pp for associate's, 2.32 pp for bachelor's and 2.87 pp for the high-school baseline.
  • S1501's 25+ totals match the release's 25+ population (which is PEP-calibrated) to a median 0.14%, max 3.94% (DC), so binding levels barely fights PEP.
  • Instrument difference. The release is below S1501 on some college in 49 of 51 states and above it on associate's in 44. This reads as a CPS-vs-ACS instrument difference, not sampling. Binding the two cells separately would move weight to settle a definition. The re-pin review should decide between binding seven cells or merging some college with associate's.
  • Thin cells. 13 cells have fewer than 30 records, 10 of them less_than_9th_grade in small states. A standalone per-state entropy calibration to the cells, which is not the release solve, fits exactly. It keeps at least 82.6% of each state's Kish ESS, but its largest weight ratio is 131 (LA). Merging the two below-diploma classes thickens those cells.
  • The held-out 18-to-24 universe is off by a mean absolute 6.81 pp, as the residence-concept difference predicts.

What waits on chronicle#347

This PR waits only on PolicyEngine/chronicle#347 merging and the next Chronicle release, which carries the S1501 package census/acs_s1501_state_2024. Its raw file is already on R2 at #347's head 81e3f986.

  • After that, the US feed re-pin (us/chronicle_feed.json, chronicle_feed_scope.json) adds the S1501 pairs, and the parity manifest regenerates and declares census_acs.acs1_2024 compiled. A release that compiles those rows must run with --cps-educational-attainment.

Everything else here works without it. Temporary draft input: only experiments/modal_297.py reads the draft package, from the fork (DTrim99/chronicle@81e3f986, labelled TEMPORARY in the file). The facts there are content-identical to a local build at 4448a7c and at 81e3f98. No lockfile, feed pin or package resource points at the fork.

SCF education-debt imputation (#1073 / PR #1246), note only

#1073's hold-out already tried SCF educ as a predictor. build_plus_education raised household rank correlation from 0.168 to 0.207 but worsened the share-with-debt error from -0.30 pp to -0.92 pp. This column supplies the host side: map SCF educ and the host reference person's class to the same seven classes. The share error argues for adding it alongside the FSA borrower-count calibration (#1074), not as a bare predictor. #1246 is unchanged.

Modal runs (experiments/modal_297.py, app microcosm-297-experiments, volume microcosm-297-cache)

  • 20261022T171517Z-e2621e231 (fc-01M5J9R996YS9K51QVQQAFTVZ1), at this branch's head: tests (the new module, fiscal targets, the stored-input register, the Chronicle feed, labels, the data shard's stored-input tests, the builder's attainment/age/ledger-filter subset, ci_test_plan.py verify), then the engine-contract register, spec-bundle and bundle-contract tests, then the receipt. All three passed (tests 420 s, contract 489 s, receipt 123 s; peak 4.6 GB). The receipt is committed in the follow-up commit.
  • 20261022T160713Z-d25305d52 (fc-01M5J5VMMRPHA6206FSPZBFQPC): the probe of the host and the Census members that set the identity checks. The host's PH_SEQ is globalized, and source_household_id carries Census's. Every host PERIDNUM is found in its year's member.

Notes

🤖 Generated with Claude Code

…1 state cells (#297)

The fiscal-refresh build can join each person's CPS ASEC A_HGA from the
pinned Census person member (exact income year and PERIDNUM, with the Census
household, line and age checked) and classify it into the seven ACS S1501
population-25-and-over classes as cps_educational_attainment. The stored-input
register flags the column as auxiliary: carried for conditioning and
calibration, not consumed by rules.

The US target compiler binds the S1501 state 25+ detail cells as person counts
of the column, and the tool's materializer counts them. The pinned Chronicle
feed has no S1501 facts, so nothing binds until PolicyEngine/chronicle#347
merges and the feed is re-pinned. experiments/modal_297.py runs the tests and
the receipt on Modal; only it reads the draft package, from the fork.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Z-e2621e231)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant