diff --git a/changelog.d/us-reported-ssi-receipt.added.md b/changelog.d/us-reported-ssi-receipt.added.md new file mode 100644 index 0000000000..30f7318c26 --- /dev/null +++ b/changelog.d/us-reported-ssi-receipt.added.md @@ -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. diff --git a/docs/evidence/spec-engine/us-f0-coverage.json b/docs/evidence/spec-engine/us-f0-coverage.json index b6153a67b0..4a63043704 100644 --- a/docs/evidence/spec-engine/us-f0-coverage.json +++ b/docs/evidence/spec-engine/us-f0-coverage.json @@ -1887,7 +1887,7 @@ "alpha", "zeta" ], - "sha256": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007" + "sha256": "9682de0316d09df4ed7f4182c930c55736d49ba2f98220db9a1a4c872707a2f8" }, "failures": [], "observed": { @@ -1911,7 +1911,7 @@ "alpha", "zeta" ], - "sha256": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007" + "sha256": "9682de0316d09df4ed7f4182c930c55736d49ba2f98220db9a1a4c872707a2f8" }, "status": "covered" }, @@ -1975,7 +1975,7 @@ "take_up_contract", "us_qbi_reconciliation_contract" ], - "sha256": "61a5484eab9fae1d6651b3ae15f1793d4934ea2f6ed2c08d05716b745c221014" + "sha256": "ba069fa7507ff84d3ceae95dbbe84952a3714d2ae154f3f8c50a4b93c61a5892" }, "status": "covered" }, @@ -2599,7 +2599,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "740d1ff5b2e9fa6da3154c44f8676f80109d404e6aa7a16c48ed8db83055b15d" + "spec_sha256": "0887aba600be33e0eea818a36cbed142dba3df3977781577583e4d05b5a3eb26" } }, "report_schema_version": 3, @@ -2609,7 +2609,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "740d1ff5b2e9fa6da3154c44f8676f80109d404e6aa7a16c48ed8db83055b15d" + "spec_sha256": "0887aba600be33e0eea818a36cbed142dba3df3977781577583e4d05b5a3eb26" }, "status": "pass" } diff --git a/docs/evidence/us-reported-ssi-receipt/README.md b/docs/evidence/us-reported-ssi-receipt/README.md new file mode 100644 index 0000000000..11a0491cdb --- /dev/null +++ b/docs/evidence/us-reported-ssi-receipt/README.md @@ -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. diff --git a/docs/evidence/us-reported-ssi-receipt/income_fail_by_channel.py b/docs/evidence/us-reported-ssi-receipt/income_fail_by_channel.py new file mode 100644 index 0000000000..3e5f08275f --- /dev/null +++ b/docs/evidence/us-reported-ssi-receipt/income_fail_by_channel.py @@ -0,0 +1,69 @@ +"""Income-test failures among own SSI reporters, by support channel (2026-01). + +Usage (next to measure_receives_ssi.py): + PYTHONPATH= 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() diff --git a/docs/evidence/us-reported-ssi-receipt/measure_receives_ssi.py b/docs/evidence/us-reported-ssi-receipt/measure_receives_ssi.py new file mode 100644 index 0000000000..355c1606a7 --- /dev/null +++ b/docs/evidence/us-reported-ssi-receipt/measure_receives_ssi.py @@ -0,0 +1,409 @@ +"""Measure what survey-reported SSI receipt (`receives_ssi`) changes in PE-US. + +Reads the pinned default dataset (populace_us_2024.h5, sha256 6496cc43...) and +runs PolicyEngine-US on a household subset, twice per code version: + +* ``base``: the dataset as published (no ``receives_ssi`` column, so the + engine default ``False`` applies); +* ``receipt``: the same rows plus ``person.receives_ssi = SSI_VAL > 0``. + +Subset = every household containing an SSI reporter (any row with SSI_VAL > 0) +plus every Colorado household. ``receives_ssi`` is a person input and a static +microsimulation has no cross-household interaction, so households without a +reporter cannot change: the weighted receipt deltas over the subset equal the +national deltas. The script checks that claim on the Colorado non-reporter +households, whose delta must be exactly zero. Colorado totals are complete +because every Colorado household is in the subset. + +Usage (one code version per process): + PYTHONPATH= python measure_receives_ssi.py