Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion changelog.d/scf-bond-docstring.fixed.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Correct the `scf_wealth` docstring: SCF `bond` is directly held tax-exempt, mortgage-backed, U.S. government and agency, and corporate and foreign bonds. It excludes U.S. savings bonds, which the SCF reports separately as `savbnd` and this stage does not read, and bond funds, which are in `nmmf`. The docstring had said `bond` included savings bonds. Because the spec engine hashes `scf_wealth.py`'s bytes into the legacy-v1 seed-protocol attestation, the seed-protocol and seed-map digests, the US `spec_sha256`, the loader golden vector and `docs/evidence/spec-engine/us-f0-coverage.json` are re-pinned. No behaviour or data changes.
Correct the `scf_wealth` docstring: SCF `bond` is directly held tax-exempt, mortgage-backed, U.S. government and agency, and corporate and foreign bonds. It excludes U.S. savings bonds, which the SCF reports separately as `savbnd`, and bond funds, which are in `nmmf`. The docstring had said `bond` included savings bonds. Because the spec engine hashes `scf_wealth.py`'s bytes into the legacy-v1 seed-protocol attestation, the seed-protocol and seed-map digests, the US `spec_sha256`, the loader golden vector and `docs/evidence/spec-engine/us-f0-coverage.json` are re-pinned. No behaviour or data changes.
1 change: 1 addition & 0 deletions changelog.d/scf-savings-bonds.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Count U.S. savings bonds in the SCF half of `bond_assets`. The `scf_wealth` stage now maps `bond_assets` to SCF `bond` + `savbnd` instead of `bond` alone. PolicyEngine-US cites POMS SI 01140.240 (U.S. Savings Bonds) for `bond_assets`, and SIPP 2023 `TVAL_BOND`, the other half of the 50/50 blend, already includes savings bonds through government securities. On the pinned SCF 2022 extract, households with a nonzero SCF bond target rise from 1.11% to 7.36% by weight, and the weighted total rises from $1,398B to $1,699B (face value). In PolicyEngine-US 2.2.1, `bond_assets` feeds SSI countable resources, the SNAP asset test, `spm_unit_cash_assets` (read by many state TANF and local general-assistance resource tests) and Missouri TANF, so all of them see the change. This departs from archived commit `42ed5d45`, which kept `savbnd` as a separate net-worth component that microcosm does not build. The seed-kernel attestation is re-pinned, as for any byte edit to `scf_wealth.py`. Released data changes only through a rebuild and release.
16 changes: 8 additions & 8 deletions docs/evidence/spec-engine/us-f0-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -1656,13 +1656,13 @@
"compiler_ir.node_slices"
],
"expected": {
"map_sha256": "0fa116d9f5dae4ba0378f190d6c822b92f3dcf1e3fe80c9880bcaf87431c283c",
"protocol_sha256": "b4afa376b62fbc073fd10d74e40921f5ab60c1efe366e2c8504872091ea69d08"
"map_sha256": "767ac1c449018ceeb743e8990a7b7822c93051d995a63b9cb5743fdf327c5c74",
"protocol_sha256": "62b2155f6dd9383b607277218fa1acf5bfeaacb81243413bab2cc31c2e62607b"
},
"failures": [],
"observed": {
"map_sha256": "0fa116d9f5dae4ba0378f190d6c822b92f3dcf1e3fe80c9880bcaf87431c283c",
"protocol_sha256": "b4afa376b62fbc073fd10d74e40921f5ab60c1efe366e2c8504872091ea69d08"
"map_sha256": "767ac1c449018ceeb743e8990a7b7822c93051d995a63b9cb5743fdf327c5c74",
"protocol_sha256": "62b2155f6dd9383b607277218fa1acf5bfeaacb81243413bab2cc31c2e62607b"
},
"status": "covered"
},
Expand All @@ -1677,7 +1677,7 @@
"compiler_ir.seed_stream_map"
],
"expected": {
"implementation_sha256": "b4afa376b62fbc073fd10d74e40921f5ab60c1efe366e2c8504872091ea69d08",
"implementation_sha256": "62b2155f6dd9383b607277218fa1acf5bfeaacb81243413bab2cc31c2e62607b",
"protocol": "legacy-v1",
"streams": [
"build_model",
Expand All @@ -1698,7 +1698,7 @@
},
"failures": [],
"observed": {
"implementation_sha256": "b4afa376b62fbc073fd10d74e40921f5ab60c1efe366e2c8504872091ea69d08",
"implementation_sha256": "62b2155f6dd9383b607277218fa1acf5bfeaacb81243413bab2cc31c2e62607b",
"protocol": "legacy-v1",
"streams": [
"build_model",
Expand Down Expand Up @@ -2599,7 +2599,7 @@
"country": "us",
"schema_id": "country_spec",
"schema_version": 1,
"spec_sha256": "d1df6b316e4937e809f5f020ea2d3a7bd38bccc1845f3ae6e104f7eed6a73323"
"spec_sha256": "d70f33e454eaae91e1bec20f04466051a5cd574d4612912f89118ccf0a8d9ac8"
}
},
"report_schema_version": 3,
Expand All @@ -2609,7 +2609,7 @@
"country": "us",
"schema_id": "country_spec",
"schema_version": 1,
"spec_sha256": "d1df6b316e4937e809f5f020ea2d3a7bd38bccc1845f3ae6e104f7eed6a73323"
"spec_sha256": "d70f33e454eaae91e1bec20f04466051a5cd574d4612912f89118ccf0a8d9ac8"
},
"status": "pass"
}
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,8 @@
"late_schedule": "e59c019d3d454eac99ac0ac209b6c5b6faaf9bdfcaeee18c36a25be19bf7da2f",
"ownership": "5f64f0aac49e2313177564f71876bffc8c81b3ded4df701e70930e60e9c98356",
"primary_tuples": "fdf23da429f8c501e198a2f5b719763d523a41565c2153fdf17b0344f21bcd7c",
"seed_map": "0fa116d9f5dae4ba0378f190d6c822b92f3dcf1e3fe80c9880bcaf87431c283c",
"seed_protocol": "b4afa376b62fbc073fd10d74e40921f5ab60c1efe366e2c8504872091ea69d08",
"seed_map": "767ac1c449018ceeb743e8990a7b7822c93051d995a63b9cb5743fdf327c5c74",
"seed_protocol": "62b2155f6dd9383b607277218fa1acf5bfeaacb81243413bab2cc31c2e62607b",
"source_manifest": "f4f8bfeb79268a4043a843b47cec1d1c0e58e9f5dd6bc23417ada95446dd7b8e",
"take_up": "9522ce40f7dea569312dd7a7beb474e5a5afadd2f1e10cb5876534c3ef623d35",
"tail": "ac92829c88a1a4fb6460d61190918d5d99c6c377fc8dd8f62f02b332d09bf59c",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,44 @@
restores the realistic low liquid-asset mass absent from the SCF-only stage.

The SCF side uses the public summary extract (``rscfp2022.dta``) and the
regime-gated weighted QRF (``microcosm.fit.QRF``). Its targets exactly match
archived commit ``42ed5d45`` (``utils/asset_imputation.py``):
regime-gated weighted QRF (``microcosm.fit.QRF``). Its targets match archived
commit ``42ed5d45`` (``utils/asset_imputation.py``) except ``bond_assets``,
which also reads U.S. savings bonds:

- ``bank_account_assets`` ← SCF ``liq`` (checking, savings, money-market,
call accounts).
call and prepaid-card accounts; the Fed's macro sets it to at least $1 for
a household holding any such account).
- ``stock_assets`` ← SCF ``stocks`` + ``nmmf`` (directly-held stock plus
stock/non-money-market mutual funds — the one code-level summation the
canonical pipeline applies).
- ``bond_assets`` ← SCF ``bond`` (directly held tax-exempt, mortgage-backed,
U.S. government and agency, and corporate and foreign bonds). Bond funds
are in ``nmmf``, so they land in ``stock_assets``; U.S. savings bonds are
the SCF's separate ``savbnd``, which this stage does not read.
stock/non-money-market mutual funds).
- ``bond_assets`` ← SCF ``bond`` + ``savbnd``. ``bond`` is directly held
tax-exempt, mortgage-backed, U.S. government and agency, and corporate and
foreign bonds; the Fed's extract macro excludes bond funds and savings bonds
from it. ``savbnd`` is U.S. savings bonds (``X3902``). Bond funds are in
``nmmf``, so they land in ``stock_assets``.

The archive read ``bond`` alone and kept ``savbnd`` as ``scf_savings_bonds``,
a construction-only component of its net-worth reconciliation. Microcosm
builds none of those components (see ``net_worth`` below), so under the
archive mapping an SCF-drawn household's savings bonds reached no SSI leaf.
PolicyEngine-US documents ``bond_assets`` as "Value of bonds and government
securities. Imputed from SIPP TVAL_BOND." and cites POMS SI 01140.240 (U.S.
Savings Bonds) and SI 01140.250 (Municipal, Corporate and Government Bonds).
SIPP 2023 ``TVAL_BOND`` sums the government-securities and municipal/corporate
bond values, and the 2023 SIPP data dictionary defines government securities
as "such as savings bonds, T-Bills, T-Bonds, T-Notes" (``EOWN_GOVS``). Reading
``bond`` + ``savbnd`` gives the SCF half the same concept as the SIPP half and
the variable's cited sources. If the archive's net-worth components are ever
built, ``scf_savings_bonds`` must leave that partition, because
``bond_assets`` now carries ``savbnd``.

Two measurement limits remain, for this leaf as before. The SCF records face
values: ``X3902`` for savings bonds, and face-value items for every component
of ``bond``. SIPP asks balance or market value including interest, while POMS
counts other bonds at current market value (SI 01140.250) and savings bonds at
redemption value (SI 01140.240). And neither survey identifies savings bonds
still in their mandatory retention period (12 months for Series EE and I bonds
issued on or after 2/1/2003), which SI 01140.240 says are not resources. Both
limits affect amounts, not which leaf savings bonds belong to.

The SCF QRF's eight predictors are age, sex, race, marriage, own children,
employment income, interest/dividend income, and Social Security/pension
Expand Down Expand Up @@ -201,12 +227,14 @@
US_SCF_NET_WORTH_OUTPUT_COLUMNS
)

#: Each output column and the SCF summary-extract components it sums
#: (retired pipeline @ 42ed5d45, ``utils/asset_imputation.py``).
#: Each output column and the SCF summary-extract components it sums. These
#: are the retired pipeline's mappings (42ed5d45, ``utils/asset_imputation.py``)
#: except ``bond_assets``, which adds U.S. savings bonds (``savbnd``) as the
#: module docstring explains. No component feeds more than one leaf.
SCF_FINANCIAL_ASSET_TARGET_COMPONENTS: dict[str, tuple[str, ...]] = {
"bank_account_assets": ("liq",),
"stock_assets": ("stocks", "nmmf"),
"bond_assets": ("bond",),
"bond_assets": ("bond", "savbnd"),
}
SCF_NET_WORTH_TARGET_COMPONENTS: dict[str, tuple[str, ...]] = {
"net_worth": ("networth",),
Expand Down Expand Up @@ -262,9 +290,10 @@
_HOUSEHOLD_ID_COLUMN = "person_household_id"

#: Weighted person-level nonzero-share plausibility bands. Centred on the
#: pinned incumbent eCPS parity reference (bank 0.54, stock 0.16, bond 0.03)
#: with generous width — the gate exists to catch an all-zero or constant
#: surface, not to pin a point estimate.
#: pinned incumbent eCPS parity reference (bank 0.54, stock 0.16, bond 0.03;
#: those reference values are unweighted record shares) with generous width —
#: the gate exists to catch an all-zero or constant surface, not to pin a
#: point estimate.
_BANK_NONZERO_SHARE_BAND = (0.25, 0.85)
_STOCK_NONZERO_SHARE_BAND = (0.03, 0.40)
_BOND_NONZERO_SHARE_BAND = (0.001, 0.12)
Expand Down Expand Up @@ -560,6 +589,20 @@ def fetch_scf_2022_summary_extract(
return target


def _scf_financial_asset_targets(raw: pd.DataFrame) -> pd.DataFrame:
"""Sum each SSI leaf's SCF components, sentinel-cleaned, floored at zero."""

targets = pd.DataFrame(index=raw.index)
for output, components in SCF_FINANCIAL_ASSET_TARGET_COMPONENTS.items():
total = np.zeros(len(raw), dtype=np.float64)
for component in components:
total = total + _replace_sentinels(raw[component]).to_numpy(
dtype=np.float64
)
targets[output] = np.maximum(total, 0.0)
return targets


def load_scf_2022_financial_asset_donor(path: str | Path) -> pd.DataFrame:
"""Read the SCF 2022 summary extract into a financial-asset donor table.

Expand All @@ -579,10 +622,11 @@ def load_scf_2022_financial_asset_donor(path: str | Path) -> pd.DataFrame:

raw = pd.read_stata(path, convert_categoricals=False)
required = {
"liq",
"stocks",
"nmmf",
"bond",
*(
component
for components in SCF_FINANCIAL_ASSET_TARGET_COMPONENTS.values()
for component in components
),
"networth",
*_SCF_SUMMARY_PREDICTOR_SOURCE_COLUMNS,
}
Expand All @@ -592,15 +636,7 @@ def load_scf_2022_financial_asset_donor(path: str | Path) -> pd.DataFrame:
f"SCF 2022 summary extract missing required column(s): {missing}."
)

donor = pd.DataFrame(index=raw.index)
# Targets: component sums, sentinel-cleaned, floored at zero.
for output, components in SCF_FINANCIAL_ASSET_TARGET_COMPONENTS.items():
total = np.zeros(len(raw), dtype=np.float64)
for component in components:
total = total + _replace_sentinels(raw[component]).to_numpy(
dtype=np.float64
)
donor[output] = np.maximum(total, 0.0)
donor = _scf_financial_asset_targets(raw)
# SCF networth is already the complete signed balance-sheet aggregate.
# Preserve negative values: they are indebted-household source signal, not
# missing-value sentinels.
Expand Down Expand Up @@ -1014,9 +1050,10 @@ def _financial_assets_carry_signal(person: pd.DataFrame) -> bool:
Nonfinite values mark a corrupted surface and force re-imputation. The
engine-default check is JOINT across the three leaves: a surface where
every leaf is constant (the all-zero engine default) must be re-imputed,
but a single legitimately constant leaf must not force a redraw — bond
holdings are ~97% zero in the donor, so a small or chunked frame can
draw a constant bond column from a perfectly healthy imputation. A
but a single legitimately constant leaf must not force a redraw — about
93% of SCF donor households by survey weight hold neither bonds nor
savings bonds, so a small or chunked frame can draw a constant bond
column from a perfectly healthy imputation. A
per-leaf nonconstancy test made pass-through platform-dependent (the
#510 CI failure) and would silently redraw small production chunks;
flattened cross-leaf uniqueness would accept three DISTINCT constant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_semantic_hash_has_golden_vector_and_surface_separation(tmp_path) -> Non
# Pin the domain separator, normalization rules, schema-set receipt, and
# exact normative projection as one reviewable golden vector.
assert first.spec_sha256 == (
"6273f82f0e4899e97d1bede4db0662829b3df6f420dba32c0e67018d010c2c91"
"8d578ce26106423171bfe57be0cf36026c6c6febd1b4e572d784c3cf96409550"
)

second_root = _rich_minimal(tmp_path / "xy", note="second", store="local:b")
Expand Down
Loading
Loading