Repository navigation
Conversation
Add the scf_education_debt runtime module. It carries federal education debt from the 2025 SCF through two weighted QRFs and allocates it to the SCF-style reference person and spouse as four person inputs: federal_student_loan_balance and federal_parent_plus_loan_balance (policyengine-us#9721), is_income_based_repayment_new_borrower and federal_student_loan_graduate_share (policyengine-us#9724). - Only loans flagged federal count; X7179 is spread over loans 1-6 in proportion to their balances; whose-education code 3 is Parent PLUS on the reference person, codes 6, 7 and -7 the reference person's own. - scf_wealth declares the two pinned 2025 files as artifacts 3 and 4 with a new scf_2025 vintage. Net worth, liquid assets and auto loans stay on 2022. - The fiscal-refresh tool runs the stage only with --scf-education-debt. The four columns join the stored-input register with the pending- engine reason the parent ids carry; the coverage manifest is left until the engine pin defines them. - Regenerate sources, vintages and calibration YAML and re-pin the frozen source_stages.json and calibration projection digests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Weighted fit of the stage against SCF 2025: source checks against the Fed's Alternate Table 13 (19.92%, $25,500, matched exactly), a five-fold family hold-out with a predictor ablation, and the stage imputed onto the published default populace-us-2024-spm-20260915 at its weights. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codes the CPS recipient side the way the SCF defines it: married includes cohabiting partners, children count the partner's children, and the reference person follows the householder-couple rule. These move Parent PLUS from 21.5% to about 16% of imputed federal balances (SCF 13.8%) but leave the borrower-household share at about 14.2% (SCF 18.2%). Adds diagnosis and experiment scripts and their first receipts. The predictor-shift and remedy runs still need to run (on Modal; local memory is too low); the fit receipt and PR body are not updated yet. Part of #1073. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The first host fit drew federal education debt for 14.4% of CPS households against 18.2% in SCF 2025, and gave Parent PLUS 21.5% of balances against 13.8%, while the SCF hold-out reproduced the SCF. The cause is covariate shift: the CPS host carries tax-record imputations of interest and dividends and of Social Security and pensions, which put 42% and 18% of 25-34 households above zero against 16% and 6% of SCF families. Mapping each to its SCF distribution at equal rank moves the host's borrower share most of any predictor; all together reach the hold-out level (receipts/1073-predictor-shift-diagnosis.json). - SCF_EDUCATION_DEBT_CANDIDATE_PREDICTORS keeps every predictor both sides build; SCF_EDUCATION_DEBT_PREDICTORS, used by both QRFs, leaves out the two shifted income components, each with its reason in SCF_EDUCATION_DEBT_EXCLUDED_PREDICTORS. - The recipient definition sums income over the reference person, spouse or partner and their children, the CPS counterpart of the SCF primary economic unit, on top of the SCF-matching codings. - The stage spec drift check now covers both QRFs; the stage notes, module docstring and changelog say why. - The receipt builder reports the build, the first receipt and the all-candidates setting side by side and records commit and run id. - experiments/modal_1073.py runs the heavy steps on Modal, adds generate and spec steps, and collects every tracked file a run changed. Re-pins the frozen source_stages.json digest; the generated mirrors are regenerated on Modal in a follow-up commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…edictors (#1073) Output of tools/generate_us_bundle_from_constants.py (run on Modal at b9f3d67, run 20261020T154855Z-b9f3d6719): both education-debt QRFs drop interest_dividend_income and social_security_pension_income, the stage notes follow the module, and the stage-asset digest moves. The Modal experiment runner now strips the Modal runtime's PYTHONPATH from step environments; inheriting it put Modal's own packages beside the venv's and the worker-identity check refused the duplicate idna. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_a_spouse_draw_without_a_spouse_moves_to_the_reference_person checks where a spouse's draw lands. With the synthetic donor's random incidence and a 30-household frame, the SCF-comparable predictor set draws no debt for any couple on 3 of 20 seeds (seed 11 among them), while the routing assertion holds on all 20 for both predictor sets (Modal probe run 20261020T161732Z-5d047f687). Every donor family now borrows in this test. The Modal runner gains --script for an uncommitted local probe, recorded by sha256 in the run log. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… loan register entries (#1073) - experiments/1073-scf-education-debt-receipts.md and its JSON are the receipt of Modal run 20261021T133110Z-e35c4b0f9 at e35c4b0 (build, first-receipt and all-candidates settings on the published default). - experiments/modal_1073.py gains `launch`, which spawns the run and exits, so a stopped local client cannot cancel it, and `--script` for an uncommitted probe. - test_support/microcosm_data/stored_inputs.py lists the four federal student loan inputs among the producer-bound register entries, next to the parent ids: no file examined for #1026 stores them, and test_us_stored_input_register.py binds them to their live producer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ves (#1073) The scf_wealth education-debt declaration adds 145 configuration fields to the US bundle (141 authored, 4 resolved). Modal probe run 20261021T145336Z-b1a461f19 printed every value below from the code; none is derived by hand. It also diffed the field universe against origin/main: all 159 added and 14 removed pointers belong to exactly one claim, and no field is unclaimed or claimed twice. - field_usage: 42,329 configuration / 32,545 authored / 9,784 resolved (was 42,184 / 32,404 / 9,780); claim pins source_stages 1,845 (the two 2025 artifacts and six operations of scf_wealth), vintage_records 120 and resolved_vintage_authorities 67 (scf_2025), calibration 316 (the four person outputs in the QRF tail-concentration list), each with its observed digest; ledger mode and generation-0 effect counts in the field-usage and coverage-tool tests. - inventory_coverage: source_manifest, late_resource_semantics, tail and full_checkpoint digests; tail control fields 938 and resolved references 336. - The pool tool's bundle spec_sha256 (a6f254a8...). - experiments/modal_1073.py: the spec step first runs the bundle generator and the coverage attestation in --check mode. The coverage tool's pointer-inventory digest and the regenerated docs/evidence/spec-engine/us-f0-coverage.json follow in the next commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1073) Modal probe run 20261021T150509Z-fcbd704c9, with the previous commit's field-usage and inventory pins in place, printed the coverage tool's pointer-inventory digest (37ce78d6...) and regenerated docs/evidence/spec-engine/us-f0-coverage.json with tools/spec_engine_coverage.py; --check then reported 42329/42329 configuration fields and 41/41 inventory checks. The bundle spec_sha256 stays a6f254a8.... Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#1073) test_support/microcosm_build/spec_engine_inventory_coverage.py keeps its own copy of the reviewed inventory counts. Modal run 20261021T151444Z-05856e6ca reported the two the stage moves, matching inventory_coverage.py and probe run 20261021T145336Z-b1a461f19: resolved references 336 (the scf_2025 vintage) and tail control fields 938 (the four outputs in the QRF tail-concentration list). Everything else in that run passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main moved the seed protocol, the compiled seed map and the bundle spec_sha256 (microcosm#104, #1124, #1301, #1332); this branch moved the source-manifest, resource-semantics, tail and full-checkpoint digests. The conflicts keep both sides' changes; the merged tree's identity values are re-derived on Modal in the next commit, and the F0 attestation is regenerated there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Modal run 20261021T183247Z-97bb3af92 regenerated the bundle mirrors on the merge commit (no change) and re-derived every identity value there: the field-usage counts and claim pins, the ledger counts, the inventory digests and counts, and the pointer-inventory digest all match the committed values. The only moved value is the bundle spec_sha256, now 90fa3260... (main's kernel changes plus this branch's stage), which the pool tool pins. tools/spec_engine_coverage.py regenerated the attestation and --check reported 42329/42329 configuration fields and 41/41 inventory checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reran build_1073_scf_education_debt_receipt.py on Modal at 2a6e65a (run 20261022T153020Z-2a6e65a73), after the merge with main brought in #1332's QRF change. Every fit figure is unchanged; only the receipt's commit, run and date move. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…docstring Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MaxGhenis
reviewed
Oct 22, 2026
MaxGhenis
left a comment
Contributor
There was a problem hiding this comment.
Strong diagnosis: the source checks and the covariate-shift work are convincing. Before this leaves draft:
- Parent PLUS. FSA's loan-type table (chronicle#347) puts Direct Parent PLUS at $115.0B of $1,724.5B at FY2026 Q3, or 6.7%. Consolidated Parent PLUS sits inside the consolidation total, so that's a floor, but the SCF's 13.8% and the host's 16.8% are two to two and a half times it. Your docstring names one cause: an adult child's own loan is coded 3 and lands on the parent. Can you route code-3 loans to a co-resident adult child when there is one, and benchmark the result against FSA?
- IBR new borrowers. Label the 59% as an upper bound, since paid-off pre-2014 loans and consolidations dated by their newest loan both read as new. Counting Parent PLUS loans in the flag is right, because 685.209(b)(13)(ii) looks at any Direct or FFEL balance.
- Composition. Parent PLUS is the residual after the two own shares. Add a test that a donor family with no Parent PLUS rarely draws one.
- Fit. On the host, the share with debt is flatter by income than in the SCF (bottom quintile 14.3% vs 11.4%, top 14.2% vs 17.9%). Borrowers are also 22% short (21.2M vs 27.2M), and too many are aged 50 to 61 (20.8% vs 16.2%). FSA calibration fixes levels by age, state and balance, but not the income gradient. Let's talk about whether that's good enough for the distributional results.
- Reword the client's name out of the changelog fragment and the module docstring, and get CI green at the final head with the receipts rerun there.
3 tasks
… swap X8000 codes on loans 4 and 6 (#1073) Parent PLUS is drawn for itself before the own shares, the IBR new-borrower share is labelled an upper bound, and the receipt benchmarks Parent PLUS and borrowers against Federal Student Aid. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1073) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Regenerated on Modal (run 20261023T143414Z-b93cc3ec0): sources.yaml, the field-usage counts and source_stages claim pin, three inventory digests, the coverage pointer-inventory digest, the bundle spec_sha256 and the F0 attestation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… against FSA Modal run 20261023T152015Z-60255dd98 at 60255dd (tests, spec and receipt steps all passed). Parent PLUS falls from 13.8% to 9.1% of SCF federal balances and from 16.8% to 10.7% on the host; FSA's Direct Parent PLUS share is 6.7%, a floor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DTrim99
added a commit
that referenced
this pull request
Oct 23, 2026
…d portfolio facts (#1074) A new runtime module, fsa_student_loan_targets, binds Federal Student Aid's portfolio tables (the six packages/ed_fsa packages of chronicle#347) to the SCF education-debt stage's person columns, federal_student_loan_balance and federal_parent_plus_loan_balance: the balance and borrower count of each state and DC, of five borrower age bands and of nine debt-size bands, and the Parent PLUS balance beside the balance of every other loan type. Every family binds at FY2026 Q2 (31 March 2026), the latest quarter all four bound tables publish, as a stock unaged at the 2024 build period (target aging's new declared stock_at_source_reference_date alignment). Not Reported is allocated to the reported locations, Puerto Rico and Other leave with their share, and the age, debt-size and loan-type rows bind as shares of the one in-scope control ($1,679.1B, 44.31M borrowers). Repayment plans, the portfolio summary and loan-type recipient counts stay diagnostic, each with its reason in the receipt. compile_us_fiscal_target_registry binds the facts only with student_loan_targets, which the fiscal-refresh release tool sets with --scf-education-debt; the release materializes the rows from the stored person columns, guards their restated age and balance bands, records every fact's disposition in us_source_coverage.json and keeps the opt-in family off the target-parity contract. Default compiles and releases are unchanged. The ED Federal Student Aid provider joins the calibration label registries. experiments/fit_1074_fsa_calibration.py refits the published default with and without the family (plus L2-anchored arms) on Modal through experiments/modal_1074.py, against the pinned feed plus chronicle#347's FSA rows, now built at its merge commit on Chronicle main. Squashes #1357's earlier history onto #1246's review-round head. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DTrim99
added a commit
that referenced
this pull request
Oct 23, 2026
…chronicle#347 (#1074) Modal run 20261023T191527Z-4b6bfce9a at 4b6bfce. chronicle#347's export at its merge commit (acbedb5) is byte-identical to the PR head's (sha256 58774e11...), so the candidate feed and the test fixture are unchanged. With loans for a child's education on co-resident adult children, the host gains young borrowers (1,246 borrower records under 25, from 561) and its Parent PLUS share falls from 16.8% to 10.7%, but holds the same borrowers (21.2M) and borrower households (16.4%) at the published weights. Arm B now meets 95% of the FSA targets within 5% (92% before) and every age band; B + L2 78% (76%). Borrower households' share of weight after calibration is 33.7% (B) and 32.3% (B + L2), against 33.0% and 31.5% before and 18.2% of SCF families: the routing moves balances within households, so it does not shrink the doubling. Non-FSA capped error: B 3.62% (3.66%), B + L2 3.85% (3.83%), A unchanged at 3.30%. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #1073 (student debt pilot, PolicyEngine/us-student-debt#1). Draft: the four columns stay out of certified releases until policyengine-us defines them, and Max approves any release.
Review of 2026-10-22: what changed
Receipts rerun at the merged head (
60255dd98, Modal run20261023T152015Z-60255dd98; the final commit adds only the receipts and the changelog figures). FSA figures are its portfolio tables (chronicle#347): Direct Parent PLUS $115.0B of $1,724.5B at FY2026 Q3, and borrowers by age and by debt size at FY2026 Q2, which count the parent on a Parent PLUS loan.1. Parent PLUS: loans for a child go to a co-resident adult child
"Before" is the previous rule; the host's before column is the previous receipt (2a6e65a). Surveys capture about $1.0T of FSA's $1.7T, so the shares compare and the levels don't. FSA's 42.3M unduplicated recipients count the student instead of the parent.
The rule (module docstring, stage notes,
derive_scf_education_debt_targets). The 2025 screener (X7801) leaves out loans of people living elsewhere, so a code-3 loan is either a co-resident child's own loan or the parent's Parent PLUS loan. No education-loan item names the listed person, so the rule uses the household listing (persons 3-12, relationship X108...X226, age X110...X228) with the loan's year (X9332-X9337), program (X9338-X9343) and amount borrowed:The one judgment call is an undergraduate loan within the limit for a child of dependent age, which could be either; I read it as the child's own, the member the listing names. The receipt's readings table bounds it:
On the host the child's draw goes to the youngest child of the reference person or partner aged 18 or over (
PEPAR1/PEPAR2). The second QRF now conditions on the number of such children (adult_children_in_household: listing code 4 aged 18 or over on the SCF side), so a household without one rarely draws a child's share. If it does, the share becomes the head's Parent PLUS loan, the SCF side's rule for a student who lives elsewhere.Borrower counts don't move (SCF 27.2M, host 21.2M, FSA 44.9M). A child takes over a balance a parent held, and most parents whose only federal loan was for the child stop counting as borrowers (Parent PLUS holders 4.4M → 2.1M, child borrowers 0 → 2.3M on the SCF side). What moves is who the borrowers are, toward FSA's age profile.
2. X8000: verified, and loans 4 and 6 swapped back
I reran the degree test independently, by loan position and across all five implicates (
x8000_degree_checkin the receipt builder). The test takes completed degree loans coded 1 or 2 in families with a spouse or partner, keeps those consistent with exactly one partner's highest degree (X5931, X6111), and counts the ones consistent with the coded person:Each position fails on its own: loan 4 has 0 of 7 discriminating loans in implicate 1, and loan 6 has 0 of 3. I also tried an age-at-borrowing test, but too few couples have a partner too young to have borrowed, so it can't tell the cases apart.
The fix.
_scf_whose_education_codesswaps codes 1 and 2 of loans 4 and 6 when X8000 = 1 (SCF_WHOSE_EDUCATION_UNREVERSED_LOANS). It is tied to the pinned member SHA-256, since the codebook names no exception.test_reversed_families_swap_the_codes_of_loans_4_and_6_backcovers both codes, the other four loans, unreversed families, and code 3 staying put. The swap moves 0.3% of federal balances.3. IBR new borrowers: an upper bound
The 59.5% is now labelled an upper bound in the receipt row, the module docstring, the stage notes and the changelog. Two reasons:
Parent PLUS stays in the head's flag, citing 685.209(b)(13)(ii).
4. Composition: Parent PLUS is drawn, not left over
The second QRF now draws the Parent PLUS, adult child's, head's and spouse's shares in that order, then normalizes them to sum to one, so Parent PLUS is no longer the residual of the two own shares. A borrower drawn with no share holds its own loan, and whole dollars go to the largest share, so a zero share stays zero.
test_a_donor_family_without_parent_plus_rarely_draws_one: own debt splits between partners in fractions, and only families with a reference person 55 or older hold Parent PLUS. Under 2% of the others draw any, and a donor with no Parent PLUS never draws it.5. The fit discussion (your point 4)
This round changes who holds the debt, not how much each household has. The first QRF, its predictors and its seed are unchanged, so every household's drawn total is identical, and so is every share-with-debt figure, including the income gradient: bottom quintile 14.3% vs SCF 11.4%, top 14.2% vs 17.9%. The borrower age profile moves toward FSA, since parents' Parent PLUS balances become their children's own loans:
So the 50-61 excess is gone: it came from Parent PLUS balances held by parents in that band. The income gradient and the borrower-count shortfall (21.2M against SCF 27.2M and FSA 44.9M) remain for our discussion. I haven't tuned toward either.
6. Odds and ends
scfp2022s.zipre-release (10/9): nothing in this round reads the 2022 files. The tests use synthetic fixtures, and the receipts and probes read only the 2025 files. Nothing broke.What this builds
A new US runtime module,
us_runtime/scf_education_debt.py, imputes four person inputs from the 2025 SCF:federal_student_loan_balancefederal_parent_plus_loan_balanceis_income_based_repayment_new_borrowerfederal_student_loan_graduate_shareDonor. The loader joins
rscfp2025.dtaandp25i6.dtaon(y1, yy1)across all five implicates. It refuses a row-count drift, an unmatched implicate, or per-loan balances that do not reproduceedn_inst. Both files are declared as artifacts 3 and 4 ofscf_wealth, pinned by zip and member SHA-256 against a fresh Fed download (2026-10-12), with a newscf_2025vintage.Model. One weighted regime-gated QRF draws each household's federal education debt. A second, fit on borrower families and conditioning also on that total and the number of adult children, draws the chained composition: the Parent PLUS, adult child's, reference person's and spouse's shares in that order (normalized to sum to one), then the three graduate shares and new-borrower flags. Both condition on the same predictors (
SCF_EDUCATION_DEBT_PREDICTORS):The shared SCF interest-dividend and Social Security-pension predictors are left out. The cause section below says why, and
SCF_EDUCATION_DEBT_EXCLUDED_PREDICTORSrecords each reason.SCF_EDUCATION_DEBT_CANDIDATE_PREDICTORSkeeps the full list for the diagnostics. Educational attainment waits for Carry educational attainment as an auxiliary variable (imputation predictor + ACS-calibratable margin) #297.People (
SCF_EDUCATION_DEBT_RECIPIENT_DEFINITION). The CPS side builds each predictor the way the SCF defines it:is_marriedcounts a cohabiting partner, as SCFMARRIEDdoes.KIDScounts them.The reference person, the member its
A_SPOUSE(elsePECOHAB) pointer names, and the youngest child of either aged 18 or over receive the balances. A spouse draw in a household with no spouse or partner goes to the reference person, and a child's draw in a household with no adult child is the reference person's Parent PLUS loan.Release tool.
tools/build_us_fiscal_refresh_release.pyruns the stage only with--scf-education-debt, after auto loans. That run applies a signal gate and writes a design-weight fit table (artifacts/us_scf_education_debt_fit.json). Default releases are unchanged and do not fetch the 2025 files.Rules for the reviewer to confirm
1 + X7179 / (balance on loans 1-6). That applies the family's federal share of loans 1-6 to X7179, and splits its federal part among people in the same proportions. A family with X7179 but no balance on loans 1-6 contributes nothing; the receipt counts those dollars (none in 2025).federal_student_loan_balance;federal_student_loan_balance;federal_student_loan_balance, or else the reference person'sfederal_parent_plus_loan_balance, by the rule in review item 1;Known limits:
federal_student_loan_balanceexcludes until consolidated.Fit vs SCF 2025, before and after
All figures are weighted. "Hold-out" draws each SCF family from models fit on the other four of five folds, grouped by family. "Host" runs the stage with seed 0 on the published default
populace-us-2024-spm-20260915, at weights that were calibrated without these columns. The full tables, with FSA balance and age bands, race, income quintiles and medians by group, are inexperiments/1073-scf-education-debt-receipts.md(JSON inexperiments/receipts/).Source checks. The loader matches the Fed's Alternate Table 13 exactly: 19.92% of families hold education debt, with a median of $25,500. Federal loans hold 87.4% of balances on loans 1-6. X7179 holds 0.6% of all education debt.
The three host settings:
A_MARITLmarriage, own children only, whole-household income, all candidate predictors.Household figures are unchanged from the previous receipt: this round changes only who holds each household's draw.
The cause: covariate shift in imputed income components
The model reproduced the SCF out of sample but drew too few borrowers on the CPS host. A QRF reproduces its donor's conditional distribution, so a gap that appears only on the host means the host's predictors are distributed differently from the donor's. The diagnosis tested this predictor by predictor (
experiments/diagnose_1073_host_fit.py,diagnose_1073_predictor_shift.py).Your whole-household income hypothesis did not hold for 25-34. CPS household income there is lower than SCF primary-economic-unit income, not higher: median $76k vs $82k, wages $62k vs $72k. Narrower CPS units are lower still.
The CPS host carries tax-record imputations of interest and dividends and of Social Security and pensions; the SCF has survey reports. The two disagree most at 25-49, where most borrowers are:
A QRF reads those values as positions in the SCF distribution, so young host households looked like older, asset-holding SCF families. Those families seldom hold education debt.
Each predictor's shift, measured directly (
experiments/receipts/1073-predictor-shift-diagnosis.json). The diagnosis maps one predictor at a time to its SCF distribution at equal rank, within five-year reference-age bins, and redraws the host's borrower share. The as-is share is 14.2% (SCF 18.2%):Codings. Three CPS predictors were also coded differently from the SCF definitions: cohabiting partners, partners' children, and the reference person (the oldest-adult rule picks a different person from the SCF ordering in 13% of households). Fixing them and the income unit moved Parent PLUS from 21.5% to 15.5% but left the borrower share at 14.5% (v4b).
Dropping the two shifted predictors on top of v4b gave the build. The recipient-definition and predictor variants are in
experiments/receipts/1073-recipient-definition-variants.json(run before this round, so their Parent PLUS column uses the previous rule):r3 shows the income unit matters for Parent PLUS: with whole-household income it goes back to 21.1%.
Remaining gaps
Borrower households are about 1.7 points short (16.4% vs 18.2%). Most of the gap is at reference ages 25-34 (28.1% vs 36.2%), with a smaller one at 35-49 (24.4% vs 27.2%).
Parent PLUS is 10.7% of the host total vs 9.1% in the SCF and 6.7% (a floor) at FSA.
Borrowers are 21.2M on the host vs 27.2M in the SCF and 44.9M at FSA, and the share with debt is flatter by income than in the SCF (bottom quintile 14.3% vs 11.4%, top 14.2% vs 17.9%).
The FSA calibration (Calibrate US federal student loan balances to Federal Student Aid portfolio targets and show them on the dashboard #1074) reweights levels by age, state and debt size. It does not fix the split between own loans and Parent PLUS, or which households hold debt.
What is still shifted at 25-34, under the build's income unit (
receipts/1073-host-fit-diagnosis.json):These are the likely next levers. I haven't tested whether either one explains the gap.
Predictor ablation (five-fold hold-out on SCF families)
One seed and no repeated runs, so small differences may be noise.
educeducDropping the two shifted predictors costs nothing out of sample on the SCF itself. Education is still the only predictor that raises the rank correlation, by about 0.04.
How the receipts were produced
Heavy steps run on Modal through
experiments/modal_1073.py. It clones a pushed commit and runsuv sync --all-packages --extra us --frozen. It also fetches the published default (HFpolicyengine/populace-us@8ab57ffc) and the SCF 2025 zips, and checks every SHA-256. The runner saves each step's log, plus every tracked file the step changed, to a volume;collectbrings them back. Uselaunch, which spawns the run and exits, so a stopped local client cannot cancel it.experiments/1073-scf-education-debt-receipts.md+receipts/1073-scf-education-debt-fit.json20261023T152015Z-60255dd98(tests,spec,receipt)20261022T153020Z-2a6e65a7320261023T135427Z-3f47edf23,20261023T140011Z-3f47edf23(--script)spec/sources.yaml, identity values and F0 attestation20261023T143414Z-b93cc3ec0(generate, identity probe,tests); merged-tree check20261023T145651Z-60255dd98changed nothingreceipts/1073-predictor-shift-diagnosis.json,receipts/1073-recipient-definition-variants.json20261020T150445Z-47949111freceipts/1073-host-fit-diagnosis.jsonspec/sources.yaml20261020T154855Z-b9f3d6719(generate;engine_abi.lock.jsonalready current)20261021T145336Z-b1a461f19,20261021T150509Z-fcbd704c9docs/evidence/spec-engine/us-f0-coverage.json20261021T183247Z-97bb3af92(tools/spec_engine_coverage.py, then--check, on the merge with main)20261021T183247Z-97bb3af92(generatechanged nothing; only the bundlespec_sha256moved)tests,specconfirmation before the merge20261021T164853Z-83a42f85dtests,specconfirmation20261021T191640Z-2a6e65a73Scope: 2025 for education debt only
Net worth, the liquid-asset leaves and auto loans still read the 2022 files; nothing they produce changes. To move the whole
scf_wealthstage to 2025 later:fetch_scf_2022_summary_extract/fetch_scf_2022_full_extract(and theirload_*callers) atSCF_2025_SUMMARY_EXTRACT/SCF_2025_FULL_EXTRACT, usingfetch_scf_public_file;source_stages.jsonand repoint thescf_2022vintage;The predictor and target code needs no change. The 2025 summary extract carries every column the shared predictor construction reads (
age,hhsex,racecl5,married,kids,wageinc,intdivinc,ssretinc, plusliq,stocks,nmmf,bond,networth). The 2025 codebook still lists the auto-loan items (X2209...X7170).racecl5andracecl4fold the new Middle Eastern or North African category into White non-Hispanic.Release plumbing and the engine contract
The four variables are not on policyengine-us
main, so:US_STORED_NON_VARIABLE_COLUMNSwith the same pending-engine reasonparent_1_id/parent_2_idcarry.test_us_stored_input_register.pybinds them to their producer (US_SCF_EDUCATION_DEBT_OUTPUT_COLUMNS).test_support/microcosm_data/stored_inputs.pylists them as producer-bound, because no file examined for Published US releases store would_claim_wic, which policyengine-us 2.x ignores, so WIC take-up is 100% under policyengine.py 6.1.1 #1026 stores them. The entries leave when the pinned engine carries #9721 and #9724.assert_release_input_coverage_manifest_currentrequires every declared column to be a live engine input leaf, and arequiredcolumn would refuse every default release. I did not work around that check. The entries belong in the PR that moves the policyengine-us pin.Spec identity, re-derived from the code, never by hand. The probe also diffed the field universe against
origin/main. All 159 added and 14 removed pointers belong to exactly one claim, and no field is unclaimed or claimed twice.source_stages.jsondigest.sources.yaml,vintages.yaml(scf_2025) andcalibration.yaml: the four outputs join the QRF tail-concentration list, which skips absent columns.source_stages(1,884),vintage_records(120),resolved_vintage_authorities(67) andcalibration(316).source_manifest,late_resource_semantics,tail,full_checkpoint; this round moved all buttail) and two counts: tail controls 938, resolved references 336.spec_sha256:eee022d2…(it was90fa3260…last round; merging main this round moved nothing).Tests
Modal (Linux, Python 3.14,
--extra us), run20261023T152015Z-60255dd98at 60255dd, after merging main:--checkmode (42,368/42,368 configuration fields, 41/41 inventory checks);Every command passed.
GitHub CI: green at the final head 3eb3232 (run 39991184753): lint, wheels, select-countries, engine-free (3.13, 3.14), engine-us (3.13, 3.14), engine-uk (3.13, 3.14) and integration-uk. The run at 60255dd was superseded by the push of the receipts.
Follow-ups
spec_engine/seeds.py) when it joins the default graph. Today they use a dedicatedSeedSequence([seed, 1073, step])stream.🤖 Generated with Claude Code