Skip to content

Impute federal student loan balances from the 2025 SCF (#1073) - #1246

Draft
DTrim99 wants to merge 19 commits into
mainfrom
scf-2025-education-debt
Draft

DTrim99 wants to merge 19 commits into
mainfrom
scf-2025-education-debt

Conversation

@DTrim99

@DTrim99 DTrim99 commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

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 run 20261023T152015Z-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

FSA SCF 2025, before SCF 2025 Host build, before Host build
Parent PLUS share of federal balances 6.7% (floor) 13.8% 9.1% 16.8% 10.7%
Parent PLUS balances $115.0B $138.3B $91.2B $143.9B $91.8B
Borrowers (people) 44.9M 27.2M 27.2M 21.2M 21.2M
Borrowers aged 24 and younger 13.8% 5.7% 10.9% 5.0% 12.2%
Borrowers aged 25 to 34 31.2% 33.1% 35.3% 29.2% 31.1%
Borrowers aged 35 to 49 33.5% 38.1% 37.2% 37.8% 37.2%
Borrowers aged 50 to 61 14.3% 16.2% 11.2% 20.8% 13.7%
Borrowers aged 62 and older 7.1% 6.9% 5.4% 7.2% 5.8%

"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 child's own loan when the listing has a child (code 4) aged 18 or over who could have been in school when the loan was borrowed: 16 or older that year, at the oldest age the child's age band allows. The public file bands listed ages, so ages 18-27 all read 25.
  • Unless only a parent could have borrowed it. That is an undergraduate or certificate loan above the $31,000 aggregate limit on a dependent undergraduate's own Direct Loans (34 CFR 685.203(e)(1)), borrowed while the child could still have been under 24 and so dependent (20 U.S.C. 1087vv(d)(1)(A)).
  • Never Parent PLUS: a graduate loan, or one borrowed after every fitting child turned 24. Parent PLUS finances only a dependent undergraduate (34 CFR 685.200(c)(2)(i)).
  • Every other code-3 loan is the head's Parent PLUS loan. No listed adult child could have borrowed it, so the student lives elsewhere, or it exceeds the dependent limit.

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:

Reading (SCF 2025) Parent PLUS share Child's own share Parent PLUS holders Child borrowers
Before: every code-3 loan Parent PLUS 13.8% 0.0% 4.4M 0
Build 9.1% 4.7% 2.1M 2.3M
Within-limit loans for a dependent-age child also Parent PLUS 11.8% 2.0% 4.1M 0.4M

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_check in 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:

Loans X8000 Discriminating Consistent with the coded person
1, 2, 3, 5 not reversed 388 378
1, 2, 3, 5 reversed 543 540
4, 6 not reversed 25 25
4, 6 reversed 54 (10 in implicate 1) 0

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_codes swaps 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_back covers 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:

  • the SCF sees only loans still owed, so a borrower whose pre-2014 loans were paid off reads as new;
  • a consolidated loan is dated by its newest underlying loan. 29.3% of federal balances dated 2014 or later are on a consolidated or refinanced loan (X9320-X9325).

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.
  • New hold-out column: 3.0% of held-out SCF borrower families observed without Parent PLUS draw some. The hold-out's Parent PLUS share is 6.8% against 9.1% observed, so the draw now errs low.

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:

Host borrower age Before Now SCF 2025 FSA
24 and younger 5.0% 12.2% 10.9% 13.8%
50 to 61 20.8% 13.7% 11.2% 14.3%

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

  • Client name: removed from the changelog and docstring in 3f47edf.
  • CI: green at the final head 3eb3232 (run 39991184753).
  • scfp2022s.zip re-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:

Column policyengine-us What it holds
federal_student_loan_balance #9721 The person's own federal loans
federal_parent_plus_loan_balance #9721 Federal loans for a child's education that only the parent could hold
is_income_based_repayment_new_borrower #9724 Every federal loan the person holds was borrowed in 2014 or later (an upper bound)
federal_student_loan_graduate_share #9724 Share of the person's own original principal that financed a graduate or professional program
  • Donor. The loader joins rscfp2025.dta and p25i6.dta on (y1, yy1) across all five implicates. It refuses a row-count drift, an unmatched implicate, or per-loan balances that do not reproduce edn_inst. Both files are declared as artifacts 3 and 4 of scf_wealth, pinned by zip and member SHA-256 against a fresh Fed download (2026-10-12), with a new scf_2025 vintage.

  • 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 reference person's age, sex and race;
    • whether a spouse or partner is present;
    • the number of children;
    • wages, and income less capital gains.

    The shared SCF interest-dividend and Social Security-pension predictors are left out. The cause section below says why, and SCF_EDUCATION_DEBT_EXCLUDED_PREDICTORS records each reason. SCF_EDUCATION_DEBT_CANDIDATE_PREDICTORS keeps 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:

    • The reference person is the CPS householder or, with a spouse or cohabiting partner present, the man of a mixed-sex couple or else the older partner. This is the SCF's ordering of its economically dominant couple.
    • is_married counts a cohabiting partner, as SCF MARRIED does.
    • Children are those of the reference person or partner, as SCF KIDS counts them.
    • Incomes sum over the reference person, spouse or partner and their children. That is the CPS counterpart of the SCF primary economic unit.

    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 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.py runs 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. Federal only. Loans with X7879/X7884/X7889/X7894/X7899/X7994 = 1 count. A blank federal flag (some 2025 "don't know" answers) counts as non-federal.
  2. X7179 (total owed on loans beyond the sixth; it has no federal or whose-education item). Each loan 1-6 is scaled by 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).
  3. Whose education (X7978, X7883, X7888, X7893, X7898, X7993):
    • code 1 (respondent; the public file swaps codes so this is the SCF reference person) → the reference person's federal_student_loan_balance;
    • code 2 (spouse or partner) → the spouse's federal_student_loan_balance;
    • X8000: in reversed families the public file left loans 4 and 6 unreversed, so their codes 1 and 2 are swapped back (review item 2);
    • code 3 (child; the public file folds grandchild and other relative into 3) → a co-resident adult child's own federal_student_loan_balance, or else the reference person's federal_parent_plus_loan_balance, by the rule in review item 1;
    • codes 6 (multiple people), 7 (unrelated person) and -7 (other) → the reference person's own balance. None can be a Parent PLUS loan for one dependent student, and the reference person is the household's default borrower. None occurs on a federal loan in the 2025 public file.
  4. IBR new borrower. True when every federal loan the person holds, own and Parent PLUS, was borrowed in 2014 or later. 2014 counts as after July 1, as asked. For a consolidated loan the year is the most recent underlying loan's. The share is an upper bound (review item 3).
  5. Graduate share. Programs 13-15 (master's, professional, doctorate) over all own federal loans (the child's too), weighted by amount borrowed (X7805...X7951), falling back to the balance when that is missing. Parent PLUS loans are excluded.
  6. The two excluded predictors (below). Wages and income less capital gains stay in, because the issue asks for income and they carry the income gradient.

Known limits:

  • The SCF asks about the loans of the family living in the household, so independent adults living in someone else's household are missed unless the family reports the loan as one for a child.
  • No loan item names the listed person, and the public file bands listed ages (18-27 read 25), so the child rule works from age bands, and with two or more adult children the youngest holds the loan.
  • The SCF federal flag includes FFEL and Perkins loans that federal_student_loan_balance excludes until consolidated.
  • Balances are 2025 survey-date dollars.

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 in experiments/1073-scf-education-debt-receipts.md (JSON in experiments/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:

  • First receipt: oldest-adult reference person, A_MARITL marriage, own children only, whole-household income, all candidate predictors.
  • v4b: the SCF codings and the couple-and-children income unit, all candidate predictors.
  • Build: v4b without the interest-dividend and Social Security-pension predictors.
SCF 2025 Hold-out (build) Host: first receipt Host: v4b Host: build
Households with federal education debt 18.2% 17.9% 14.4% 14.5% 16.4%
Parent PLUS share of the total 9.1% 6.8% 13.0% 10.7% 10.7%
Mean among borrower households $41,052 $38,156 $44,483 $42,174 $42,680
Median among borrower households $25,000 $23,000 $25,000 $25,000 $23,000
Weighted total $1.006T $0.919T $0.786T $0.747T $0.858T
Borrowers (reference persons, spouses and adult children) 27.2M 25.8M 18.8M 18.6M 21.2M
Median balance per borrower $21,000 $21,000 $24,700 $24,000 $22,224
Share with debt, reference person 25-34 36.2% 36.9% 26.7% 27.1% 28.1%
Share with debt, reference person 35-49 27.2% 27.4% 22.3% 20.9% 24.4%
Share with debt, income quintile 4 26.6% 26.3% 20.0% 19.2% 23.1%
IBR new borrowers among borrowers (upper bound) 59.5% 58.4% 57.9% 59.7% 61.3%
Graduate share of own original principal 47.2% 44.7% 50.8% 48.8% 50.0%
Error vs SCF: share of households (pp) -0.30 -3.74 -3.70 -1.74
Error vs SCF: reference-age-band share MAE (pp) 1.26 3.55 4.17 2.39
Error vs SCF: income-quintile share MAE (pp) 0.68 4.25 4.57 2.87
Error vs SCF: race share MAE (pp) 1.07 2.83 2.57 0.94

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:

    Share with some Interest/dividends 25-34 35-49 SS/pensions 25-34 35-49
    CPS host households 42% 43% 18% 20%
    SCF families 16% 23% 6% 12%

    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%):

    Predictor mapped Host borrower share
    interest/dividends 15.8%
    household income 15.1%
    SS/pensions 14.7%
    wages 14.5%
    children 14.5%
    all together 17.3%, the hold-out's level
  • 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):

Variant Host borrower share Parent PLUS Hold-out share error (pp)
v4b 14.45% 15.5% -0.87
r1: v4b without interest/dividends 16.07% 16.1% -0.73
r2: also without SS/pensions (build) 16.41% 16.8% -0.30
r3: r2 with whole-household income 16.47% 21.1% -0.30

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):

    • wages: host median $48k vs $72k for SCF primary economic units;
    • spouse or partner present: 45% of host households vs 54% of SCF families.

    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.

Predictors Share error (pp) Mean error Balance-band TVD Age-band MAE (pp) Race MAE (pp) Income-quintile MAE (pp) Borrower-age TVD Rank corr.
shared 8 without age -0.25 -1.7% 0.022 4.81 0.65 0.67 0.107 0.154
shared 8 -0.53 -4.1% 0.041 1.19 1.08 0.88 0.016 0.173
shared 8 + household income (first receipt) -0.87 -4.3% 0.037 1.35 1.36 0.92 0.009 0.175
+ SCF educ -1.47 -5.6% 0.040 1.32 2.07 1.47 0.023 0.208
build (no interest/dividends, SS/pensions) -0.30 -7.1% 0.032 1.26 1.07 0.68 0.015 0.168
build + SCF educ -0.92 -7.1% 0.028 1.75 1.79 0.92 0.031 0.207

Dropping 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 runs uv sync --all-packages --extra us --frozen. It also fetches the published default (HF policyengine/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; collect brings them back. Use launch, which spawns the run and exits, so a stopped local client cannot cancel it.

Artifact Modal run Commit
experiments/1073-scf-education-debt-receipts.md + receipts/1073-scf-education-debt-fit.json 20261023T152015Z-60255dd98 (tests,spec,receipt) 60255dd, this round's merge with main
previous receipts (the "before" columns) 20261022T153020Z-2a6e65a73 2a6e65a
SCF probes behind the child rule and the X8000 check 20261023T135427Z-3f47edf23, 20261023T140011Z-3f47edf23 (--script) 3f47edf
this round's spec/sources.yaml, identity values and F0 attestation 20261023T143414Z-b93cc3ec0 (generate, identity probe, tests); merged-tree check 20261023T145651Z-60255dd98 changed nothing b93cc3e, 60255dd
receipts/1073-predictor-shift-diagnosis.json, receipts/1073-recipient-definition-variants.json 20261020T150445Z-47949111f 4794911
receipts/1073-host-fit-diagnosis.json local run, 2026-10-12 branch at that time
regenerated spec/sources.yaml 20261020T154855Z-b9f3d6719 (generate; engine_abi.lock.json already current) b9f3d67
field-usage and inventory identity values probes 20261021T145336Z-b1a461f19, 20261021T150509Z-fcbd704c9 b1a461f, fcbd704
docs/evidence/spec-engine/us-f0-coverage.json 20261021T183247Z-97bb3af92 (tools/spec_engine_coverage.py, then --check, on the merge with main) 97bb3af
identity values on the merged tree 20261021T183247Z-97bb3af92 (generate changed nothing; only the bundle spec_sha256 moved) 97bb3af
tests,spec confirmation before the merge 20261021T164853Z-83a42f85d 83a42f8
final tests,spec confirmation 20261021T191640Z-2a6e65a73 2a6e65a

Scope: 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_wealth stage to 2025 later:

  • point fetch_scf_2022_summary_extract / fetch_scf_2022_full_extract (and their load_* callers) at SCF_2025_SUMMARY_EXTRACT / SCF_2025_FULL_EXTRACT, using fetch_scf_public_file;
  • drop the two 2022 artifacts from source_stages.json and repoint the scf_2022 vintage;
  • re-check the net-worth and asset plausibility bands, then re-derive the frozen digest and the spec-engine identity values as this PR does.

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, plus liq, stocks, nmmf, bond, networth). The 2025 codebook still lists the auto-loan items (X2209...X7170). racecl5 and racecl4 fold 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:

  • Stored-input register. They join US_STORED_NON_VARIABLE_COLUMNS with the same pending-engine reason parent_1_id/parent_2_id carry. test_us_stored_input_register.py binds them to their producer (US_SCF_EDUCATION_DEBT_OUTPUT_COLUMNS). test_support/microcosm_data/stored_inputs.py lists 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.
  • Coverage manifest: not added. assert_release_input_coverage_manifest_current requires every declared column to be a live engine input leaf, and a required column would refuse every default release. I did not work around that check. The entries belong in the PR that moves the policyengine-us pin.
  • No certified release from this PR.

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.

  • The frozen source_stages.json digest.
  • sources.yaml, vintages.yaml (scf_2025) and calibration.yaml: the four outputs join the QRF tail-concentration list, which skips absent columns.
  • The field-usage counts: 42,368 configuration fields, 32,584 authored and 9,784 resolved (42,329 / 32,545 / 9,784 before this round; 42,184 / 32,404 / 9,780 on main).
  • The claim pins source_stages (1,884), vintage_records (120), resolved_vintage_authorities (67) and calibration (316).
  • The ledger mode and effect counts.
  • Four inventory-coverage digests (source_manifest, late_resource_semantics, tail, full_checkpoint; this round moved all but tail) and two counts: tail controls 938, resolved references 336.
  • The coverage pointer-inventory digest.
  • The pool tool's bundle spec_sha256: eee022d2… (it was 90fa3260… last round; merging main this round moved nothing).
  • The F0 attestation.

Tests

  • Modal (Linux, Python 3.14, --extra us), run 20261023T152015Z-60255dd98 at 60255dd, after merging main:

    • the stage's 42 unit tests, including the X8000 swap, nine child-routing cases, the child targets, the Parent PLUS composition test, routing to the youngest adult child and the adult-children count;
    • the bundle generator and the coverage attestation in --check mode (42,368/42,368 configuration fields, 41/41 inventory checks);
    • the engine-free spec-engine, bundle, register, plan, SCF and spine-blindness tests;
    • the release tool's education-debt tests;
    • the engine-contract spec-engine, bundle, register, SCF, lineage and release-input-coverage tests.

    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

🤖 Generated with Claude Code

DTrim99 and others added 10 commits October 12, 2026 13:47
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>
@DTrim99 DTrim99 closed this Oct 21, 2026
@DTrim99 DTrim99 reopened this Oct 21, 2026
DTrim99 and others added 4 commits October 21, 2026 14:31
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 MaxGhenis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strong diagnosis: the source checks and the covariate-shift work are convincing. Before this leaves draft:

  1. 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?
  2. 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.
  3. 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.
  4. 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.
  5. 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.

DTrim99 and others added 5 commits October 23, 2026 10:23
… 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

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.

2 participants