Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0cabbc9
Impute federal student loan balances from the 2025 SCF (#1073)
DTrim99 Oct 12, 2026
c0ccdda
Add SCF 2025 education-debt fit receipts (#1073)
DTrim99 Oct 12, 2026
4794911
WIP: SCF-matching recipient definitions and #1073 host-fit diagnostics
DTrim99 Oct 20, 2026
b9f3d67
Condition the education-debt QRFs on SCF-comparable predictors (#1073)
DTrim99 Oct 20, 2026
5d047f6
Regenerate the US stage spec for the SCF-comparable education-debt pr…
DTrim99 Oct 20, 2026
e35c4b0
Make the spouse-routing test independent of draw chance (#1073)
DTrim99 Oct 20, 2026
b1a461f
Collect the e35c4b0f receipts, add a Modal launch entrypoint, bind th…
DTrim99 Oct 21, 2026
fcbd704
Re-derive the spec-engine identity values the education-debt stage mo…
DTrim99 Oct 21, 2026
05856e6
Pin the coverage pointer inventory and regenerate the F0 attestation …
DTrim99 Oct 21, 2026
83a42f8
Re-pin the inventory test's resolved-reference and tail-control count…
DTrim99 Oct 21, 2026
97bb3af
Merge origin/main into scf-2025-education-debt (#1073)
DTrim99 Oct 21, 2026
2a6e65a
Re-derive the merged tree's bundle digest and F0 attestation (#1073)
DTrim99 Oct 21, 2026
e6070ae
Pin the #1073 fit receipt to the merged commit
DTrim99 Oct 22, 2026
3f47edf
Describe #1073 as the student debt pilot in the changelog and module …
DTrim99 Oct 22, 2026
04e8270
Route loans for a child's education to co-resident adult children and…
DTrim99 Oct 23, 2026
b93cc3e
Keep the education-debt stage operations within the sources schema (#…
DTrim99 Oct 23, 2026
a5f0749
Re-derive the spec identity values for the child-loan routing (#1073)
DTrim99 Oct 23, 2026
60255dd
Merge origin/main into scf-2025-education-debt (#1073)
DTrim99 Oct 23, 2026
3eb3232
Rerun the #1073 receipts at the merged head and benchmark Parent PLUS…
DTrim99 Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions changelog.d/1073-scf-2025-education-debt.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
The US build can impute federal student loan balances from the 2025 Survey of Consumer Finances (microcosm#1073, the student debt pilot). A new runtime module, `scf_education_debt`, writes four person inputs: `federal_student_loan_balance` and `federal_parent_plus_loan_balance` (policyengine-us#9721), and `is_income_based_repayment_new_borrower` and `federal_student_loan_graduate_share` (policyengine-us#9724).

Only education debt reads the 2025 SCF. Net worth, the liquid-asset leaves and auto loans stay on the 2022 SCF. The `scf_wealth` stage declares the two 2025 files (`scfp2025s.zip`, `scf2025s.zip`), pinned by zip and member SHA-256 against a fresh Federal Reserve download, as artifacts 3 and 4, and a `scf_2025` vintage points at them. Moving the whole stage to 2025 later changes pinned coordinates, not code: the 2025 summary extract carries every column the shared predictor construction reads.

The donor. Only loans flagged federal count. X7179, the total owed beyond the sixth loan, is spread over loans 1-6 in proportion to their balances, so it takes the family's federal share and holder split. Whose education decides the holder: the reference person's and the spouse's own loans go to `federal_student_loan_balance`, and codes 6 (multiple people), 7 (unrelated person) and -7 (other) go to the reference person's own balance. In families where the public file reversed respondent and spouse (X8000 = 1), the codes of loans 4 and 6 were left unreversed (none of 54 degree-discriminating loans match the coded person, against all 25 in other families), so the stage swaps them back. A loan for a child's education is the co-resident child's own loan when the household listing has a child aged 18 or over who could have been in school when it was borrowed, unless only a parent could have borrowed it: an undergraduate loan above the $31,000 limit on a dependent student's own Direct Loans, borrowed while the child could still have been dependent. Graduate loans and loans borrowed after the child turned 24 are never Parent PLUS. Every other loan for a child is the reference person's `federal_parent_plus_loan_balance`. This takes the Parent PLUS share of federal balances from 13.8% to 9.1% in the SCF and from 16.8% to 10.7% on the host, against 6.7% (a floor) in Federal Student Aid's portfolio, and the share of borrowers aged 24 or younger from 5.0% to 12.2% on the host (FSA: 13.8%). A person is an IBR new borrower when every federal loan they hold, Parent PLUS included, was borrowed in 2014 or later; that share is an upper bound, since paid-off pre-2014 loans are not observed and a consolidated loan is dated by its newest underlying loan. The graduate share weights each own loan's program by the amount borrowed.

The model. One weighted QRF draws each household's federal education debt. A second, fit on borrower families and conditioning on that total and the number of adult children, draws the Parent PLUS, adult child's, reference person's and spouse's shares in that order, normalized to sum to one, then the graduate shares and new-borrower flags. Parent PLUS is drawn for itself, so a family like the donors without it rarely draws it. Both condition on the reference person's age, sex and race, a spouse or partner being present, the children of the reference person or partner, and the wages and income less capital gains of the reference person, spouse or partner and their children. The CPS side codes each predictor the way the SCF defines it: SCF `MARRIED` counts cohabiting partners, SCF `KIDS` counts the partner's children, the reference person comes from the householder couple (the man, or the older partner), and income covers the primary economic unit rather than the whole household. The shared interest-dividend and Social Security-pension predictors are left out (`SCF_EDUCATION_DEBT_EXCLUDED_PREDICTORS`). The CPS host carries tax-record imputations of both, so at reference ages 25-34 it reports them for 42% and 18% of households against 16% and 6% of SCF families, and that shift was the main reason the first host fit drew too few borrowers. The reference person, the member its `A_SPOUSE` (else `PECOHAB`) pointer names, and the youngest child of either aged 18 or over receive the balances; a child's draw in a household without an adult child is the reference person's Parent PLUS loan. A signal gate refuses negative, constant or incoherent columns and a borrower-household share outside 8-35%.

The fiscal-refresh release tool runs the stage only with `--scf-education-debt`, because the pinned policyengine-us does not define the four inputs yet. The four columns join `US_STORED_NON_VARIABLE_COLUMNS` with the pending-engine reason the parent ids carry, and leave it when the engine pin moves. They are not in the release input-coverage manifest, whose check requires every column to be an engine input. No release is certified with them.

Fit receipts are in `experiments/1073-scf-education-debt-receipts.md`, generated by `experiments/build_1073_scf_education_debt_receipt.py` on Modal through `experiments/modal_1073.py`, which clones a pushed commit, verifies every input's SHA-256 and records the run id in the receipt. The diagnosis behind the predictor and recipient choices is in `experiments/receipts/1073-predictor-shift-diagnosis.json` and `1073-recipient-definition-variants.json`.
66 changes: 33 additions & 33 deletions docs/evidence/spec-engine/us-f0-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"version": 1
},
"country": "us",
"documentation_sha256": "0931cbd3aae2abcd8c072b78eba40c1c006b1bf7fa23c6a6681f9a895e47070e",
"documentation_sha256": "e978ca7b15daf6eacf424fcb039f63c68b7eeabc1fb0c686b2715c75686b8c89",
"field_usage": {
"authored_normative_field_count": 32404,
"authored_normative_field_count": 32584,
"claim_count": 49,
"claims": [
{
Expand Down Expand Up @@ -108,8 +108,8 @@
],
"mode": "legacy_behavior",
"pointer_class": "all",
"pointer_count": 312,
"pointer_sha256": "9bad55b4945af1c4213c510942e7b9d22d104bf75b0d9804e4a183e1656c8393",
"pointer_count": 316,
"pointer_sha256": "0212e52e2bf1e5996fe6b4785b2d74f2fb5680ec38b258c6ad4dbeadbd181a1b",
"rationale": null,
"relative_sink_prefix": "/calibration_contract",
"source_prefix": "/authored/spec~1calibration.yaml",
Expand Down Expand Up @@ -514,8 +514,8 @@
],
"mode": "legacy_behavior",
"pointer_class": "all",
"pointer_count": 1716,
"pointer_sha256": "3e808ed9f5a791c0dfb85be0c59adf803366bf6e8c7a9f5a58f92974e044b607",
"pointer_count": 1884,
"pointer_sha256": "886eca845bf2e67788b19c28aa24459b9e3a5edb5f2d8129fb47d13804eca393",
"rationale": null,
"relative_sink_prefix": "/source_manifest/stages",
"source_prefix": "/authored/spec~1sources.yaml/stages",
Expand Down Expand Up @@ -705,8 +705,8 @@
"legacy_sinks": [],
"mode": "compiler_semantic",
"pointer_class": "all",
"pointer_count": 112,
"pointer_sha256": "c69a923e893fde8a929ca087d29ded607fa9c04fd14a39d0a401ad1db845e6de",
"pointer_count": 120,
"pointer_sha256": "825b4ab09602353fc4d6c24b6f7654f7582502c7acf963b7ff6c117b07208eb4",
"rationale": null,
"relative_sink_prefix": null,
"source_prefix": "/authored/spec~1vintages.yaml",
Expand Down Expand Up @@ -764,29 +764,29 @@
"legacy_sinks": [],
"mode": "compiler_semantic",
"pointer_class": "all",
"pointer_count": 63,
"pointer_sha256": "5f2edfe9826e1200e2b8706780c9f53b7c61f01aad8cab5907f08c37a0b1ead2",
"pointer_count": 67,
"pointer_sha256": "3f49902e745368ce8df0ad338a1697b76fd3299d35959977d36a8c09690ffb10",
"rationale": null,
"relative_sink_prefix": null,
"source_prefix": "/resolved/vintage_authorities",
"verifier": "vintages"
}
],
"configuration_field_count": 42184,
"consumed_field_count": 42184,
"configuration_field_count": 42368,
"consumed_field_count": 42368,
"generation0_effect_counts": {
"legacy_behavior": 38498,
"no_generation0_effect": 3686
"legacy_behavior": 38670,
"no_generation0_effect": 3698
},
"mode_counts": {
"compiler_semantic": 27717,
"compiler_semantic": 27729,
"front_end_validation": 354,
"identity_only": 103,
"legacy_behavior": 14010
"legacy_behavior": 14182
},
"multiple_primary_use_field_count": 0,
"pointer_inventory_sha256": "8f655670b9794adae905ede64bdceba6395eeb8d642b6c5896d5c516f1f6a6a7",
"resolved_binding_field_count": 9780,
"pointer_inventory_sha256": "f26245d12f4cd77a77ab4b595c9351f6923330815a87b5734e9c80e2a0a129a9",
"resolved_binding_field_count": 9784,
"unused_field_count": 0
},
"inventory_coverage": {
Expand Down Expand Up @@ -814,7 +814,7 @@
"producer_nodes": 38,
"producer_virtual_resources": 75,
"release_rungs": 5,
"resolved_references": 334,
"resolved_references": 336,
"seed_owner_bindings": 112,
"seed_owner_rows": 54,
"seed_sites": 53,
Expand All @@ -824,7 +824,7 @@
"stacked_checkpoint_full_components": 13,
"stacked_checkpoint_pool_code_components": 19,
"stacked_checkpoint_static_components": 10,
"tail_control_fields": 934,
"tail_control_fields": 938,
"take_up_pipeline_steps": 28,
"take_up_programs": 17,
"typed_artifacts": 84,
Expand Down Expand Up @@ -917,13 +917,13 @@
"legacy_adapter.stacked_checkpoint_static_components"
],
"expected": {
"sha256": "ac92829c88a1a4fb6460d61190918d5d99c6c377fc8dd8f62f02b332d09bf59c",
"terminal_fields": 934
"sha256": "b3e7289f8107138e23e8d7c9a172285afd2099d45558d253cc369eebe8ba3406",
"terminal_fields": 938
},
"failures": [],
"observed": {
"sha256": "ac92829c88a1a4fb6460d61190918d5d99c6c377fc8dd8f62f02b332d09bf59c",
"terminal_fields": 934
"sha256": "b3e7289f8107138e23e8d7c9a172285afd2099d45558d253cc369eebe8ba3406",
"terminal_fields": 938
},
"status": "covered"
},
Expand Down Expand Up @@ -1361,12 +1361,12 @@
],
"expected": {
"producer_count": 38,
"sha256": "cc34ba11288d01145fc2dc1a669df3f7c4ef2019d73afb02dd8eeee852b16d20"
"sha256": "429406ee38dbdcf5d751a86e37004636931b69f772e346fd8c853510ba54b05d"
},
"failures": [],
"observed": {
"producer_count": 38,
"sha256": "cc34ba11288d01145fc2dc1a669df3f7c4ef2019d73afb02dd8eeee852b16d20"
"sha256": "429406ee38dbdcf5d751a86e37004636931b69f772e346fd8c853510ba54b05d"
},
"status": "covered"
},
Expand Down Expand Up @@ -1775,12 +1775,12 @@
"legacy_adapter.imputation.late_producer_resource_semantics"
],
"expected": {
"sha256": "f4f8bfeb79268a4043a843b47cec1d1c0e58e9f5dd6bc23417ada95446dd7b8e",
"sha256": "ef7ed58e0170f21db8be7812f99251e702bb6b429aae463a80ea0a870c16ced5",
"stages": 37
},
"failures": [],
"observed": {
"sha256": "f4f8bfeb79268a4043a843b47cec1d1c0e58e9f5dd6bc23417ada95446dd7b8e",
"sha256": "ef7ed58e0170f21db8be7812f99251e702bb6b429aae463a80ea0a870c16ced5",
"stages": 38
},
"status": "covered"
Expand Down Expand Up @@ -1887,7 +1887,7 @@
"alpha",
"zeta"
],
"sha256": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007"
"sha256": "0af421c69e29c23eedd42b1d1e890b8223d637101b8b681e8fa0d07124591b72"
},
"failures": [],
"observed": {
Expand All @@ -1911,7 +1911,7 @@
"alpha",
"zeta"
],
"sha256": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007"
"sha256": "0af421c69e29c23eedd42b1d1e890b8223d637101b8b681e8fa0d07124591b72"
},
"status": "covered"
},
Expand Down Expand Up @@ -1975,7 +1975,7 @@
"take_up_contract",
"us_qbi_reconciliation_contract"
],
"sha256": "61a5484eab9fae1d6651b3ae15f1793d4934ea2f6ed2c08d05716b745c221014"
"sha256": "fec68ba41c34b4752e4bf7cbe31e35adcaa0e89be95ae522026a3a892883419e"
},
"status": "covered"
},
Expand Down Expand Up @@ -2599,7 +2599,7 @@
"country": "us",
"schema_id": "country_spec",
"schema_version": 1,
"spec_sha256": "19460fc168a6f9ed44e580d0e36777cce78bb33cb52b75630ee86203fa795684"
"spec_sha256": "eee022d2e258e9e7dfe6b87b80f4b731e3a679486a36abe016db5ec7515dfe10"
}
},
"report_schema_version": 3,
Expand All @@ -2609,7 +2609,7 @@
"country": "us",
"schema_id": "country_spec",
"schema_version": 1,
"spec_sha256": "19460fc168a6f9ed44e580d0e36777cce78bb33cb52b75630ee86203fa795684"
"spec_sha256": "eee022d2e258e9e7dfe6b87b80f4b731e3a679486a36abe016db5ec7515dfe10"
},
"status": "pass"
}
Loading
Loading