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
7 changes: 7 additions & 0 deletions changelog.d/us-reported-ssi-receipt.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Carry reported SSI receipt as the `receives_ssi` person input.

- **CPS rows:** `cps_carried.reported_ssi_receipt` sets it from a positive `SSI_VAL`, excluding respondents whose only recorded reasons are SSI received on behalf of a child.
- **ACS rows:** `map_acs_native_inputs` maps it natively from a positive `SSIP`. Both spines measure it, so the multispine pool never transfers it.
- **Release gate:** the US release input-coverage gate requires it with no reviewed exclusion. The gate also checks that every marked person reports a positive amount of their own and that every marked CPS-measured person carries `takes_up_ssi_if_eligible`.
- **Preflight:** treats those row-dependent failures as at risk on the L0 path.
- **Published default:** `populace-us-2024-spm-20260915` carries no `receives_ssi` and now fails the gate on it.
10 changes: 5 additions & 5 deletions docs/evidence/spec-engine/us-f0-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -1887,7 +1887,7 @@
"alpha",
"zeta"
],
"sha256": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007"
"sha256": "9682de0316d09df4ed7f4182c930c55736d49ba2f98220db9a1a4c872707a2f8"
},
"failures": [],
"observed": {
Expand All @@ -1911,7 +1911,7 @@
"alpha",
"zeta"
],
"sha256": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007"
"sha256": "9682de0316d09df4ed7f4182c930c55736d49ba2f98220db9a1a4c872707a2f8"
},
"status": "covered"
},
Expand Down Expand Up @@ -1975,7 +1975,7 @@
"take_up_contract",
"us_qbi_reconciliation_contract"
],
"sha256": "61a5484eab9fae1d6651b3ae15f1793d4934ea2f6ed2c08d05716b745c221014"
"sha256": "ba069fa7507ff84d3ceae95dbbe84952a3714d2ae154f3f8c50a4b93c61a5892"
},
"status": "covered"
},
Expand Down Expand Up @@ -2599,7 +2599,7 @@
"country": "us",
"schema_id": "country_spec",
"schema_version": 1,
"spec_sha256": "740d1ff5b2e9fa6da3154c44f8676f80109d404e6aa7a16c48ed8db83055b15d"
"spec_sha256": "0887aba600be33e0eea818a36cbed142dba3df3977781577583e4d05b5a3eb26"
}
},
"report_schema_version": 3,
Expand All @@ -2609,7 +2609,7 @@
"country": "us",
"schema_id": "country_spec",
"schema_version": 1,
"spec_sha256": "740d1ff5b2e9fa6da3154c44f8676f80109d404e6aa7a16c48ed8db83055b15d"
"spec_sha256": "0887aba600be33e0eea818a36cbed142dba3df3977781577583e4d05b5a3eb26"
},
"status": "pass"
}
104 changes: 104 additions & 0 deletions docs/evidence/us-reported-ssi-receipt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Reported SSI receipt on the pinned US default: measured effect

Run 2026-10-18 with `measure_receives_ssi.py` (in this folder).
`summary_aggregates.json` holds the aggregates; it records no household-level
values.

## Setup

- **Data:** `populace_us_2024.h5`, sha256 `6496cc43…aee84`. This is
PolicyEngine-US's `DEFAULT_DATASET` (`populace-us-2024-spm-20260909`; the
same bytes as `-20260915`).
- **Model:** PolicyEngine-US `origin/main` at `fab18b7d6b` and
PolicyEngine/policyengine-us#10145 at `f450d4ab00`, with policyengine-core
3.33.2. The #10145 head branches from an older main, and the two trees
differ in 59 files, including the New Mexico and Texas supplements and
other state taxes. The receipt deltas reported here are identical on both
trees; among the reported levels, only Colorado's differs. The tables use
the #10145 head.
- **Variants:** each adds person columns to an in-memory copy of the dataset:

| Variant | Change |
|---|---|
| `base` | None; `receives_ssi` takes the engine default `False`. |
| `receipt` | `receives_ssi` from positive `SSI_VAL`, excluding on-behalf-of-child-only payees (`RESNSSI1` 3/4 with `RESNSSI2` 0/3/4). The pinned default has no row whose only payee code is in `RESNSSI2`, so this equals `cps_carried.reported_ssi_receipt` on it. |
| `receipt_asec_only` | The same, on ASEC rows only; PUF tax-detail clones stay `False`. |
| `receipt_any` | `SSI_VAL > 0` without the payee exclusion. |
| `criteria_anchor` | Diagnostic: `meets_ssi_disability_criteria = True` for under-65 own-reason reporters on the PUF channel; no receipt. |
| `receipt_criteria_anchor` | `receipt` plus `criteria_anchor`. |
| `receipt_assets0` | Diagnostic upper bound: `receipt`, with no liquid assets for anyone in a reporter's household. |

- **Households simulated:** every household with a reporter (any row with
`SSI_VAL > 0`) plus every Colorado household (2,541 households and 7,764
rows).

`receives_ssi` is a person input, and the static model has no interaction
between households, so households without a reporter cannot change. The
weighted deltas below are therefore the national deltas. The run checks this
on Colorado's non-reporter households, whose deltas are exactly zero on every
aggregate except the `medicaid` dollar value. That value spreads a fixed
total over enrollees, so it is not reported. Colorado totals are complete; the
main-branch Colorado supplement reproduces the US hub's full-dataset $233.22M.

The variants change inputs only; nothing is recalibrated. These are
fixed-weight sensitivities on today's default, not the effects of a rebuilt
default. In a rebuild the SSI take-up priors are re-solved against SSA
counts, so the federal SSI changes in the two diagnostic variants overstate
a rebuilt effect. The default has no ACS rows, so the ACS mapping does not
enter these numbers.

## Survey reporters versus modeled SSI (2026, `base`)

| | Weighted people | Modeled SSI |
|---|---:|---:|
| Own-reason SSI reporters | 5.15M | 2.79M paid; $28.4B against $49.1B reported |
| On-behalf-of-child payees (excluded) | 0.45M | 0.16M paid $1.5B of their own |

In January 2026, 2.35M own-reason reporters receive no modeled payment:

| First failing test | Weighted people |
|---|---:|
| Not aged, blind or disabled (under 65 with no `meets_ssi_disability_criteria`) | 1.03M |
| Countable income at or above the benefit | 0.78M |
| Resource test | 0.45M (median countable resources $9,087) |
| Not aged, blind or disabled (other) | 0.09M |

By channel, ASEC rows anchor `meets_ssi_disability_criteria` for every
under-65 reporter (2.48M of 2.48M). PUF tax-detail clones do not (0.82M of
1.95M), although they carry the reporter's `SSI_VAL` and the take-up anchor.
Among reporters who fail the income test, ASEC rows have a median countable
income of $1,750 a month (464,000 people, 65% with Social Security). PUF
clones have $5,265 a month (316,000 people), from PUF-imputed incomes
(`income_fail_by_channel.py`).

## Effect of adding the column (2026, change from `base`)

| Measure | `receipt` | `receipt_asec_only` | `receipt_any` | `criteria_anchor` | `receipt_criteria_anchor` | `receipt_assets0` |
|---|---:|---:|---:|---:|---:|---:|
| Federal `ssi` | $0 | $0 | $0 | +$6,240M | +$6,240M | +$1,927M |
| `household_net_income` | +$339M | +$141M | +$454M | +$5,444M | +$5,729M | +$2,066M |
| `snap` | +$261M | +$97M | +$310M | −$607M | −$397M | +$48M |
| `early_head_start` | +$1,156M | +$433M | +$1,329M | +$268M | +$1,156M | +$1,156M |
| `head_start` | $0 | $0 | +$93M | $0 | $0 | $0 |
| `free_school_meals` | +$63M | +$43M | +$121M | $0 | +$63M | +$65M |
| `aca_ptc` | −$863M | −$220M | −$923M | −$201M | −$786M | −$863M |
| `medicaid_enrolled` (people) | +1,408k | +369k | +1,576k | +350k | +1,393k | +1,443k |
| `is_ssi_recipient_for_medicaid` (people) | +2,228k | +761k | +2,510k | +566k | +2,205k | +2,316k |
| `mi_ssp` | +$6.6M | $0 | +$9.5M | $0 | +$5.0M | +$3.3M |
| `wa_ssp` | +$7.6M | +$0.3M | +$7.6M | +$10.6M | +$18.2M | +$13.2M |
| `wy_ssp` | +$0.3M | +$0.3M | +$0.3M | $0 | +$0.3M | +$0.3M |
| `co_state_supplement` | $0 | $0 | $0 | $0 | $0 | $0 |

`ca_state_supplement`, `ma_state_supplement`, `nj_ssp`, `pa_ssp` and `me_ssp`
move only in the diagnostic variants. The 2024 deltas have the same signs.
`summary_aggregates.json` has both years for both commits.

## Colorado

Main pays Colorado's AND-CS supplement ($233.22M in 2026) to 11 records. None
of them belongs to a household with a reported SSI amount: they are
modeled-eligible people with `takes_up_ssi_if_eligible = False`. #10145 pays
$0 with or without `receives_ssi`. Every Colorado reporter with positive
weight who passes AND-CS's disability and age test already has modeled SSI
plus countable income equal to the $994 grant standard, so the supplement is
$0.
69 changes: 69 additions & 0 deletions docs/evidence/us-reported-ssi-receipt/income_fail_by_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Income-test failures among own SSI reporters, by support channel (2026-01).

Usage (next to measure_receives_ssi.py):
PYTHONPATH=<pe-us tree> python income_fail_by_channel.py
"""

import measure_receives_ssi as m
import numpy as np
import pandas as pd
from policyengine_us import Microsimulation


def weighted_median(values: pd.Series, weights: pd.Series) -> float | None:
if not len(values):
return None
order = np.argsort(values.to_numpy())
cumulative = np.cumsum(weights.to_numpy()[order])
index = np.searchsorted(cumulative, cumulative[-1] / 2)
return float(values.to_numpy()[order][index])


def main() -> None:
ds, _reporter_households, _co_households = m.subset_dataset(False)
sim = Microsimulation(dataset=ds, dataset_end_year=2026)
month = "2026-01"
frame = pd.DataFrame(
{
"w": sim.calculate("person_weight", 2026).values,
"eligible": sim.calculate("is_ssi_eligible", month).values,
"uncapped": sim.calculate("uncapped_ssi", month).values,
"countable_income": sim.calculate("ssi_countable_income", month).values,
"earned": sim.calculate("ssi_earned_income", month).values,
"unearned": sim.calculate("ssi_unearned_income", month).values,
"social_security": sim.calculate("social_security", 2026).values / 12,
"employment": sim.calculate("employment_income", 2026).values / 12,
}
)
frame["channel"] = ds.person["person_support_channel"].astype(str).to_numpy()
frame["reporter"] = m.own_ssi_receipt(ds.person)
frame["ssi_val_monthly"] = pd.to_numeric(ds.person["SSI_VAL"]).to_numpy() / 12
reporters = frame[frame.reporter]
fail = reporters[reporters.eligible.astype(bool) & (reporters.uncapped <= 0)]
for channel, group in fail.groupby("channel"):
weight = group.w
print(
channel,
"income-test failures",
round(weight.sum()),
"of reporters",
round(reporters.loc[reporters.channel == channel, "w"].sum()),
"| weighted median monthly countable income",
round(weighted_median(group.countable_income, weight)),
"unearned",
round(weighted_median(group.unearned, weight)),
"Social Security",
round(weighted_median(group.social_security, weight)),
"reported SSI",
round(weighted_median(group.ssi_val_monthly, weight)),
"| share with Social Security",
round(
float((weight * (group.social_security > 0)).sum() / weight.sum()), 3
),
"| share with wages",
round(float((weight * (group.employment > 0)).sum() / weight.sum()), 3),
)


if __name__ == "__main__":
main()
Loading
Loading